Adjust AssertToSpaceInvariantInNonMovingSpace for Sticky-Bit CC.

Do not rely on the mark bitmap to check the to-space invariant in
non-moving spaces during a minor (sticky-bit) CC collection when the
young-generation collector is still scanning the dirty cards, because
of a potential race between bitmap marking (in
ConcurrentCopying::MarkNonMoving) and to-space invariant checking (in
AssertToSpaceInvariantInNonMovingSpace). Check that the read barrier
state is gray instead.

Test: ART_USE_GENERATIONAL_CC=true art/test.py
Bug: 113858074
Bug: 67628039
Change-Id: Ifa13458bc2e28a2ef8ace1496f2e1e90b0a020e4
diff --git a/runtime/gc/collector/concurrent_copying.cc b/runtime/gc/collector/concurrent_copying.cc
index 3a80008..0c18767 100644
--- a/runtime/gc/collector/concurrent_copying.cc
+++ b/runtime/gc/collector/concurrent_copying.cc
@@ -2331,6 +2331,7 @@
   CHECK(!region_space_->HasAddress(ref)) << "obj=" << obj << " ref=" << ref;
   // In a non-moving space. Check that the ref is marked.
   if (immune_spaces_.ContainsObject(ref)) {
+    // Immune space case.
     if (kUseBakerReadBarrier) {
       // Immune object may not be gray if called from the GC.
       if (Thread::Current() == thread_running_gc_ && !gc_grays_immune_objects_) {
@@ -2344,28 +2345,68 @@
           << " updated_all_immune_objects=" << updated_all_immune_objects;
     }
   } else {
+    // Non-moving space and large-object space (LOS) cases.
     accounting::ContinuousSpaceBitmap* mark_bitmap =
         heap_mark_bitmap_->GetContinuousSpaceBitmap(ref);
     accounting::LargeObjectBitmap* los_bitmap =
         heap_mark_bitmap_->GetLargeObjectBitmap(ref);
-    bool is_los = mark_bitmap == nullptr;
-    if ((!is_los && mark_bitmap->Test(ref)) ||
-        (is_los && los_bitmap->Test(ref))) {
-      // OK.
-    } else {
-      // If `ref` is on the allocation stack, then it may not be
-      // marked live, but considered marked/alive (but not
-      // necessarily on the live stack).
-      CHECK(IsOnAllocStack(ref))
-          << "Unmarked ref that's not on the allocation stack."
-          << " obj=" << obj
-          << " ref=" << ref
-          << " rb_state=" << ref->GetReadBarrierState()
-          << " is_los=" << std::boolalpha << is_los << std::noboolalpha
-          << " is_marking=" << std::boolalpha << is_marking_ << std::noboolalpha
-          << " young_gen=" << std::boolalpha << young_gen_ << std::noboolalpha
-          << " self=" << Thread::Current();
-    }
+    bool is_los = (mark_bitmap == nullptr);
+
+    bool marked_in_non_moving_space_or_los =
+        (kUseBakerReadBarrier
+         && kEnableGenerationalConcurrentCopyingCollection
+         && young_gen_
+         && !done_scanning_.load(std::memory_order_acquire))
+        // Don't use the mark bitmap to ensure `ref` is marked: check that the
+        // read barrier state is gray instead. This is to take into account a
+        // potential race between two read barriers on the same reference when the
+        // young-generation collector is still scanning the dirty cards.
+        //
+        // For instance consider two concurrent read barriers on the same GC root
+        // reference during the dirty-card-scanning step of a young-generation
+        // collection. Both threads would call ReadBarrier::BarrierForRoot, which
+        // would:
+        // a. mark the reference (leading to a call to
+        //    ConcurrentCopying::MarkNonMoving); then
+        // b. check the to-space invariant (leading to a call this
+        //    ConcurrentCopying::AssertToSpaceInvariantInNonMovingSpace -- this
+        //    method).
+        //
+        // In this situation, the following race could happen:
+        // 1. Thread A successfully changes `ref`'s read barrier state from
+        //    non-gray (white) to gray (with AtomicSetReadBarrierState) in
+        //    ConcurrentCopying::MarkNonMoving, then gets preempted.
+        // 2. Thread B also tries to change `ref`'s read barrier state with
+        //    AtomicSetReadBarrierState from non-gray to gray in
+        //    ConcurrentCopying::MarkNonMoving, but fails, as Thread A already
+        //    changed it.
+        // 3. Because Thread B failed the previous CAS, it does *not* set the
+        //    bit in the mark bitmap for `ref`.
+        // 4. Thread B checks the to-space invariant and calls
+        //    ConcurrentCopying::AssertToSpaceInvariantInNonMovingSpace: the bit
+        //    is not set in the mark bitmap for `ref`; checking that this bit is
+        //    set to check the to-space invariant is therefore not a reliable
+        //    test.
+        // 5. (Note that eventually, Thread A will resume its execution and set
+        //    the bit for `ref` in the mark bitmap.)
+        ? (ref->GetReadBarrierState() == ReadBarrier::GrayState())
+        // It is safe to use the heap mark bitmap otherwise.
+        : (!is_los && mark_bitmap->Test(ref)) || (is_los && los_bitmap->Test(ref));
+
+    // If `ref` is on the allocation stack, then it may not be
+    // marked live, but considered marked/alive (but not
+    // necessarily on the live stack).
+    CHECK(marked_in_non_moving_space_or_los || IsOnAllocStack(ref))
+        << "Unmarked ref that's not on the allocation stack."
+        << " obj=" << obj
+        << " ref=" << ref
+        << " rb_state=" << ref->GetReadBarrierState()
+        << " is_los=" << std::boolalpha << is_los << std::noboolalpha
+        << " is_marking=" << std::boolalpha << is_marking_ << std::noboolalpha
+        << " young_gen=" << std::boolalpha << young_gen_ << std::noboolalpha
+        << " done_scanning="
+        << std::boolalpha << done_scanning_.load(std::memory_order_acquire) << std::noboolalpha
+        << " self=" << Thread::Current();
   }
 }