kotlinx.coroutines: NullPointerException when setting StateFlow value

Describe the bug

After upgrading from version 1.6.4 to 1.7.1 (we have since bumped to 1.7.2) we started seeing NullPointerException crashes when updating a StateFlow value. I am not able to reproduce this locally, we are only seeing this through Firebase reports.

This is happening on Android with org.jetbrains.kotlinx:kotlinx-coroutines-android. It’s happening on many different devices and Android versions, so it isn’t specific to one manufacturer/version.

Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.Class.isInterface()' on a null object reference
       at java.lang.Class.isAssignableFrom(Class.java:589)
       at java.lang.Class.isInstance(Class.java:542)
       at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.accessCheck(AtomicReferenceFieldUpdater.java:389)
       at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.get(AtomicReferenceFieldUpdater.java:447)
       at kotlinx.coroutines.flow.StateFlowSlot.makePending(StateFlowSlot.java:59)
       at kotlinx.coroutines.flow.StateFlowImpl.updateState(StateFlow.kt:349)
       at kotlinx.coroutines.flow.StateFlowImpl.setValue(StateFlow.kt:316)

Provide a Reproducer

We have a lot of flows, and only one specific one is crashing. The only unique thing about the flow that is causing crashes is that it holds an Enum, but I don’t know if that’s causing it.

The StateFlow is nullable, but the value set on it after initialization is never null. The code below isn’t our production code, but is how our code looks like

enum class SomeEnum {
    Apple,
    Orange
}

class Class {
    val flow: StateFlow<SomeEnum?> get() = _mutableFlow
    private val _mutableFlow: MutableStateFlow<SomeEnum?> = MutableStateFlow(null)
    
    fun updateValue(value: SomeEnum) {
        _mutableFlow.value = value
    }
}

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 45
  • Comments: 35 (3 by maintainers)

Commits related to this issue

Most upvoted comments

@yangwuan55 , @Monabr , the answer is literally on the same screen, you just need to scroll up a bit to see it: no, there are no updates, as it’s an Android problem, not the problem with our library; we don’t know what’s causing this. If you can provide a reliable reproducer (a project that consistently crashes in the emulator or at least on some specific device), please do, and we’ll try to introduce a workaround.

Specifically: https://github.com/Kotlin/kotlinx.coroutines/issues/3820#issuecomment-1714285034

The problem continues to reproduce. Android 12/13.

Caused by java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.Class.isInterface()' on a null object reference
       at java.lang.Class.isAssignableFrom(Class.java:824)
       at java.lang.Class.isInstance(Class.java:774)
       at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.accessCheck(AtomicReferenceFieldUpdater.java:421)
       at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.get(AtomicReferenceFieldUpdater.java:479)
       at kotlinx.coroutines.flow.StateFlowSlot.makePending(StateFlow.kt:2)
       at kotlinx.coroutines.flow.StateFlowImpl.updateState(StateFlow.kt:349)
       at kotlinx.coroutines.flow.StateFlowImpl.setValue(StateFlow.kt:316)

It is necessary to change synthetic private static final back to static final so that “aggressive optimizations” stop working. There was no such problem in version 1.6.4.

image As a temporary solution, we might be able to circumvent this problem by forcing changes to the atomicfu version. But I don’t know if it will cause other problems.

like this:


configurations.all {
        resolutionStrategy {
            force 'org.jetbrains.kotlinx:atomicfu:0.17.3'
        }
}

We tried this solution but sadly the issue still occurs 😞

We just started getting this problem after updating our coroutines dependency from 1.6.4 to 1.8.0-RC2. We currently only have this happening in our beta release but so far it looks like it is limited to Android 13 and 14. I did a bit of digging and wanted to share it here, I will also file a ticket with Google as this seems to be an issue with the VM or with GC maybe? 🤷

The crash originates in StateFlowSlot.makePending() which before the atomicfu update looked like this (this is decompiled Android byte code to Java:

public final void makePending() {
    Symbol symbol;
    Symbol symbol2;
    Symbol symbol3;
    Symbol symbol4;
    while (true) {
        Object obj = this._state;
        if (obj == null) {
            return;
        }
        symbol = StateFlowKt.PENDING;
        if (obj == symbol) {
            return;
        }
        symbol2 = StateFlowKt.NONE;
        boolean z16 = false;
        if (obj == symbol2) {
            AtomicReferenceFieldUpdater atomicReferenceFieldUpdater = _state$FU;
            symbol3 = StateFlowKt.PENDING;
            while (true) {
                if (!atomicReferenceFieldUpdater.compareAndSet(this, obj, symbol3)) {
                    if (atomicReferenceFieldUpdater.get(this) != obj) {
                        break;
                    }
                } else {
                    z16 = true;
                    break;
                }
            }
            if (z16) {
                return;
            }
        } else {
            AtomicReferenceFieldUpdater atomicReferenceFieldUpdater2 = _state$FU;
            symbol4 = StateFlowKt.NONE;
            while (true) {
                if (!atomicReferenceFieldUpdater2.compareAndSet(this, obj, symbol4)) {
                    if (atomicReferenceFieldUpdater2.get(this) != obj) {
                        break;
                    }
                } else {
                    z16 = true;
                    break;
                }
            }
            if (z16) {
                int i9 = k.f270577;
                ((CancellableContinuationImpl) obj).resumeWith(c0.f270561);
                return;
            }
        }
    }
}

after the update to atomicfu in 1.7.0 it looks like this:

public final void makePending() {
    Symbol symbol;
    Symbol symbol2;
    Symbol symbol3;
    Symbol symbol4;
    AtomicReferenceFieldUpdater atomicReferenceFieldUpdater = _state$volatile$FU;
    while (true) {
        Object obj = atomicReferenceFieldUpdater.get(this);
        if (obj == null) {
            return;
        }
        symbol = StateFlowKt.PENDING;
        if (obj == symbol) {
            return;
        }
        symbol2 = StateFlowKt.NONE;
        boolean z13 = false;
        if (obj == symbol2) {
            AtomicReferenceFieldUpdater atomicReferenceFieldUpdater2 = _state$volatile$FU;
            symbol3 = StateFlowKt.PENDING;
            while (true) {
                if (!atomicReferenceFieldUpdater2.compareAndSet(this, obj, symbol3)) {
                    if (atomicReferenceFieldUpdater2.get(this) != obj) {
                        break;
                    }
                } else {
                    z13 = true;
                    break;
                }
            }
            if (z13) {
                return;
            }
        } else {
            AtomicReferenceFieldUpdater atomicReferenceFieldUpdater3 = _state$volatile$FU;
            symbol4 = StateFlowKt.NONE;
            while (true) {
                if (!atomicReferenceFieldUpdater3.compareAndSet(this, obj, symbol4)) {
                    if (atomicReferenceFieldUpdater3.get(this) != obj) {
                        break;
                    }
                } else {
                    z13 = true;
                    break;
                }
            }
            if (z13) {
                int i16 = k.f159498;
                ((CancellableContinuationImpl) obj).resumeWith(c0.f159482);
                return;
            }
        }
    }
}

with the crash originating in this line: Object obj = atomicReferenceFieldUpdater.get(this);

Note: Even though StateFlow.kt had no meaningful code change between 1.6.4 and 1.7.0 because of the atomicfu update the byte code changed. This is also the reason why the forced dependency downgrade of atomicfu will not “fix” this issue, as the impacted code is already in the coroutines -core .jar file. So the downgrade during app build does not really do anything anymore.

This is where it gets funky: This code calls get on AtomicReferenceFieldUpdaterImpl with this (StateFlowSlot) instance as parameter.

@SuppressWarnings("unchecked")
public final V get(T obj) {
  accessCheck(obj);
  return (V)U.getObjectVolatile(obj, offset);
}

with

private final void accessCheck(T obj) {
  if (!cclass.isInstance(obj))
    throwAccessCheckException(obj);
}

cclass is StateFlowSlot.class because it is the first parameter in

static final /* synthetic */ AtomicReferenceFieldUpdater _state$FU = AtomicReferenceFieldUpdater.newUpdater(StateFlowSlot.class, Object.class, "_state");

and the constructor of AtomicReferenceFieldUpdaterImpl assigns it to the tclass, which is the fist parameter:

this.cclass = (Modifier.isProtected(modifiers) &&
  tclass.isAssignableFrom(caller) &&
  !isSamePackage(tclass, caller))
  ? caller : tclass;

so this accessCheck is checking if our instance of type StateFlowSlot is an instance of StateFlowSlot.class

on Android 13 the isInstance call looks like this:

public boolean isInstance(Object obj) {
  if (obj == null) {
    return false;
  }
  return isAssignableFrom(obj.getClass());
}

so this will get the class from the object (which itself is not null) that we called this with (which would be the existing instance of StateFlowSlow.

into

public boolean isAssignableFrom(Class<?> cls) {
  if (this == cls) {
    return true;  // Can always assign to things of the same type.
  } else if (this == Object.class) {
    return !cls.isPrimitive();  // Can assign any reference to java.lang.Object.
  } else if (isArray()) {
    return cls.isArray() && componentType.isAssignableFrom(cls.componentType);
  } else if (isInterface()) {
    // Search iftable which has a flattened and uniqued list of interfaces.
    Object[] iftable = cls.ifTable;
    if (iftable != null) {
      for (int i = 0; i < iftable.length; i += 2) {
        if (iftable[i] == this) {
          return true;
        }
     }
    }
  return false;
  } else {
    if (!cls.isInterface()) {
      for (cls = cls.superClass; cls != null; cls = cls.superClass) {
        if (cls == this) {
         return true;
       }
      }
    }
  return false;
  }
}

And this is where the crash is in line if (!cls.isInterface()) {.

Which means that the obj.getClass() call in isInstance returned a null class even though the instance is not null.

The Object implementation on Android seems to use some kind of backing field here:

public final Class<?> getClass() {
  return shadow$_klass_;
}

which I guess can be null, maybe a GC issue? Anyway, I will also file this with Google

Android standard lib code examples from: https://android.googlesource.com/platform/libcore/+/refs/heads/android13-d1-release/ojluni/src/main/java/java/lang

Any updates? Is there a temporary solution to avoid collapse?

Thanks you so much for the invitation to me

On Sat, 9 Mar 2024, 10:07 am mengrong.yang, @.***> wrote:

We’ve merged a potential temporary workaround that is going to be included in the next release.

Please note that the workaround is temporary, and we expect to rollback it once Google has identified and fixed the issue (which we assume it will taking its severity and priority: https://issuetracker.google.com/issues/325123736)

So glad hear that,thank you so much!

— Reply to this email directly, view it on GitHub https://github.com/Kotlin/kotlinx.coroutines/issues/3820#issuecomment-1986732050, or unsubscribe https://github.com/notifications/unsubscribe-auth/BFURIDYEZZ3TFA4KEVS66S3YXKKI5AVCNFSM6AAAAAA2SU43TWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSOBWG4ZTEMBVGA . You are receiving this because you are subscribed to this thread.Message ID: @.***>

We’ve merged a potential temporary workaround that is going to be included in the next release.

Please note that the workaround is temporary, and we expect to rollback it once Google has identified and fixed the issue (which we assume it will taking its severity and priority: https://issuetracker.google.com/issues/325123736)

So glad hear that,thank you so much!

The original crash is happening on the main thread, and shouldn’t be very close to process instantiation. The second is called from the IO dispatcher, but I don’t think this should be close to process instantation either. The second is also a lot rarer, only happening 3 times compared to 80 for the original

Seeing the same crash occasionally after an upgrade to 1.7.1.

In our case the flow receives a kotlin object implementing a sealed class. The flow is updated from a dedicated dispatcher created out of Executors.newSingleThreadExecutor().

   private sealed class Status {
    data class WithSomeData(val someData: Map<String, String>) : Status()
    object NoData : Status()
    object Initial: Status()
  }

  private val status = MutableStateFlow<Status>(Initial)

  fun updateStateFlow(coroutineScope: Scope) { 
    coroutineScope.launch { 
        someOtherFlow 
         .onEach { status.emit(NoData) } // <= Crashes on this line
         .flowOn(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
         .collect()
    }
  }
java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.Class.isInterface()' on a null object reference
        at java.lang.Class.isAssignableFrom(Class.java:589)
        at java.lang.Class.isInstance(Class.java:542)
        at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.accessCheck(AtomicReferenceFieldUpdater.java:421)
        at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.get(AtomicReferenceFieldUpdater.java:479)
        at kotlinx.coroutines.flow.StateFlowSlot.makePending(Unknown:2)
        at kotlinx.coroutines.flow.StateFlowImpl.updateState(StateFlow.kt:349)
        at kotlinx.coroutines.flow.StateFlowImpl.setValue(StateFlow.kt:316)
        at kotlinx.coroutines.flow.StateFlowImpl.emit(StateFlow.kt:373)

The same stateflow is updated with WithSomeData in another code-path, but from the main thread, and that code-path has not been seen in the stacktraces.