Skip to content

fix: Don't overwrite MainThreadId #2165

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open

Conversation

bitsandfoxes
Copy link
Contributor

@bitsandfoxes bitsandfoxes commented May 19, 2025

Fixes #2164

The Issue

Based on #2164 the SDK seems to run into a race condition with IsMainThread().

I might have been able to reproduce it locally, once by:

  1. Initializing a "plugin" with RuntimeInitializeOnLoadMethod as well.
  2. What gets initialized first based on 1. - the SDK or my repro-plugin - is undefined
  3. MainThreadData.CollectData gets called twice. Once by AndroidNative.Configure, once by SentyUnity.Init
  4. The ScopeObserver is already setup, IsMainThread falsely resolves to false and SentryJava attempts to detach from the JNI

My assumption is that calling multiple calls to CollectData assign and overwrite the nullable MainThreadId and the non-atomic check

public static bool IsMainThread()
    => MainThreadId.HasValue && Thread.CurrentThread.ManagedThreadId == MainThreadId;

creates a race condition even if the same integer is getting set.

Proposal

The MainThreadId should not be overwritten once set

The current best practice for libraries to handle IsMainThread is to fetch the current thread ID at a place where it is guaranteed to be the main thread. Those are methods like Awake, Start, and the RuntimeInitializeOnLoadMethod.

On Android, when setting up the ScopeObserver, the SDK already requires IsMainThread. Since we know it's getting called through the SentryInitialization, we keep collecting it there.

The subsequent call during Init does not overwrite it.

Make IsMainThread nullable && SentryJava update for IsMainThread

In case of user manually setting up or initializing, the SentryJava needs to be able to be certain whether it is running on the main thread or not. A false positive and the attempt to detach on the main thread crashes the game.

@bitsandfoxes bitsandfoxes changed the title fix: Create SentryMainThreadData fix: Don't overwrite MainThreadId May 20, 2025
Copy link
Collaborator

@limbonaut limbonaut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with the sentry-unity codebase, so take it with a grain of salt. I fail to see a fix for the issue. Is making IsMainThreadId nullable somehow does it? I'm not entirely sure how overwriting an integer creates a racing condition in the first place. It's the same integer and writing int should be atomic, no? Read-write can be a racing condition, but in this case, we're writing the same integer value and not computing anything. From the problem statement, it seems that MainThreadId is probably not initialized in time, right? I wonder if it can be initialized even earlier.

=> MainThreadId.HasValue && Thread.CurrentThread.ManagedThreadId == MainThreadId;
public static bool? IsMainThread()
{
if (MainThreadId.HasValue)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it’s an integer that should be set once and visible across threads, I think declaring it as volatile makes sense:

internal static volatile int _mainThreadId;

If you really need to make it nullable, then this should also work:

private static int? _mainThreadId;

internal static int? MainThreadId
{
    get => Volatile.Read(ref _mainThreadId);
    set => Volatile.Write(ref _mainThreadId, value);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SIGABORT in Android NDK in version 3.2.0
2 participants