[vm, gc] Reapply the generational barrier to unforwarded slots during become.
The remembered bit may be incorrectly false when become is the result of aborting a scavenge in the middle of visiting the remembered set.
Bug: https://github.com/dart-lang/sdk/issues/43642
Bug: https://github.com/dart-lang/sdk/issues/43862
Change-Id: I32b6ca3d4627fceef3fbee21997f326e1271a48e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/170641
Reviewed-by: Martin Kustermann <kustermann@google.com>
Reviewed-by: Siva Annamalai <asiva@google.com>
Commit-Queue: Ryan Macnak <rmacnak@google.com>
diff --git a/runtime/tests/vm/dart/scavenger_abort_2_test.dart b/runtime/tests/vm/dart/scavenger_abort_2_test.dart
new file mode 100644
index 0000000..e3183da
--- /dev/null
+++ b/runtime/tests/vm/dart/scavenger_abort_2_test.dart
@@ -0,0 +1,288 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import "dart:io";
+import "package:expect/expect.dart";
+
+// The sizes of these classes are co-prime multiples of the allocation unit to
+// increase the likelihood that scavenging fails from fragmentation.
+
+// header + 13 fields = 7 allocation units
+class A {
+ dynamic field1;
+ dynamic field2;
+ dynamic field3;
+ dynamic field4;
+ dynamic field5;
+ dynamic field6;
+ dynamic field7;
+ dynamic field8;
+ dynamic field9;
+ dynamic field10;
+ dynamic field11;
+ dynamic field12;
+ dynamic field13;
+
+ // Prevent fields from being optimized away as write-only.
+ String toString() {
+ return field1 +
+ field2 +
+ field3 +
+ field4 +
+ field5 +
+ field6 +
+ field7 +
+ field8 +
+ field9 +
+ field10 +
+ field11 +
+ field12 +
+ field13;
+ }
+}
+
+// header + 17 fields = 9 allocation units
+class B {
+ dynamic field1;
+ dynamic field2;
+ dynamic field3;
+ dynamic field4;
+ dynamic field5;
+ dynamic field6;
+ dynamic field7;
+ dynamic field8;
+ dynamic field9;
+ dynamic field10;
+ dynamic field11;
+ dynamic field12;
+ dynamic field13;
+ dynamic field14;
+ dynamic field15;
+ dynamic field16;
+ dynamic field17;
+
+ // Prevent fields from being optimized away as write-only.
+ String toString() {
+ return field1 +
+ field2 +
+ field3 +
+ field4 +
+ field5 +
+ field6 +
+ field7 +
+ field8 +
+ field9 +
+ field10 +
+ field11 +
+ field12 +
+ field13 +
+ field14 +
+ field15 +
+ field16 +
+ field17;
+ }
+}
+
+// header + 19 fields = 10 allocation units
+class C {
+ dynamic field1;
+ dynamic field2;
+ dynamic field3;
+ dynamic field4;
+ dynamic field5;
+ dynamic field6;
+ dynamic field7;
+ dynamic field8;
+ dynamic field9;
+ dynamic field10;
+ dynamic field11;
+ dynamic field12;
+ dynamic field13;
+ dynamic field14;
+ dynamic field15;
+ dynamic field16;
+ dynamic field17;
+ dynamic field18;
+ dynamic field19;
+
+ // Prevent fields from being optimized away as write-only.
+ String toString() {
+ return field1 +
+ field2 +
+ field3 +
+ field4 +
+ field5 +
+ field6 +
+ field7 +
+ field8 +
+ field9 +
+ field10 +
+ field11 +
+ field12 +
+ field13 +
+ field14 +
+ field15 +
+ field16 +
+ field17 +
+ field18 +
+ field19;
+ }
+}
+
+class Old {
+ dynamic next;
+ dynamic new1;
+ dynamic new2;
+ dynamic new3;
+ dynamic new4;
+ dynamic new5;
+ dynamic new6;
+ dynamic new7;
+ dynamic new8;
+ dynamic new9;
+ dynamic new10;
+ dynamic new11;
+ dynamic new12;
+ dynamic new13;
+ dynamic new14;
+ dynamic new15;
+ dynamic new16;
+ dynamic new17;
+ dynamic new18;
+ dynamic new19;
+ dynamic new20;
+ dynamic new21;
+ dynamic new22;
+ dynamic new23;
+ dynamic new24;
+ dynamic new25;
+ dynamic new26;
+ dynamic new27;
+ dynamic new28;
+ dynamic new29;
+ dynamic new30;
+ dynamic new31;
+
+ // Prevent fields from being optimized away as write-only.
+ String toString() {
+ return new1 +
+ new2 +
+ new3 +
+ new4 +
+ new5 +
+ new6 +
+ new7 +
+ new8 +
+ new9 +
+ new10 +
+ new11 +
+ new12 +
+ new13 +
+ new14 +
+ new15 +
+ new16 +
+ new17 +
+ new18 +
+ new19 +
+ new20 +
+ new21 +
+ new22 +
+ new23 +
+ new24 +
+ new25 +
+ new26 +
+ new27 +
+ new28 +
+ new29 +
+ new30 +
+ new31;
+ }
+}
+
+fill(old) {
+ // Note the allocation order is different from the field order. The objects
+ // will be scavenged in field order, causing the objects to be rearranged to
+ // produce new-space fragmentation.
+ old.new1 = new C();
+ old.new4 = new C();
+ old.new7 = new C();
+ old.new10 = new C();
+ old.new13 = new C();
+ old.new16 = new C();
+
+ old.new2 = new B();
+ old.new5 = new B();
+ old.new8 = new B();
+ old.new11 = new B();
+ old.new14 = new B();
+ old.new17 = new B();
+
+ old.new3 = new A();
+ old.new6 = new A();
+ old.new9 = new A();
+ old.new12 = new A();
+ old.new15 = new A();
+ old.new18 = new A();
+}
+
+makeOld() {
+ // 2/4 MB worth of Old.
+ print("PHASE1");
+ var head;
+ for (var i = 0; i < 16384; i++) {
+ var old = new Old();
+ old.next = head;
+ head = old;
+ }
+
+ // 32/64 MB worth of new objects, all directly reachable from the
+ // remembered set.
+ print("PHASE2");
+ for (var old = head; old != null; old = old.next) {
+ fill(old);
+ }
+
+ print("PHASE3");
+ return head;
+}
+
+main(List<String> argsIn) async {
+ if (argsIn.contains("--testee")) {
+ // Trigger OOM.
+ // Must read the fields to prevent the writes from being optimized away. If
+ // the writes are optimized away, most of the tree is collectible right away
+ // and we timeout instead of triggering OOM.
+ print(makeOld());
+ return;
+ }
+
+ var exec = Platform.executable;
+ var args = Platform.executableArguments +
+ [
+ "--new_gen_semi_max_size=4" /*MB*/,
+ "--old_gen_heap_size=15" /*MB*/,
+ "--verbose_gc",
+ "--verify_store_buffer",
+ Platform.script.toFilePath(),
+ "--testee"
+ ];
+ print("+ $exec ${args.join(' ')}");
+
+ var result = await Process.run(exec, args);
+ print("Command stdout:");
+ print(result.stdout);
+ print("Command stderr:");
+ print(result.stderr);
+
+ Expect.equals(255, result.exitCode,
+ "Should see runtime exception error code, not SEGV");
+
+ Expect.isTrue(
+ result.stderr.contains("Unhandled exception:\nOut of Memory") ||
+ result.stderr.contains("Unhandled exception:\r\nOut of Memory"),
+ "Should see the Dart OutOfMemoryError");
+
+ Expect.isFalse(result.stderr.contains("error: Out of memory"),
+ "Should not see the C++ OUT_OF_MEMORY()");
+}
diff --git a/runtime/tests/vm/dart_2/scavenger_abort_2_test.dart b/runtime/tests/vm/dart_2/scavenger_abort_2_test.dart
new file mode 100644
index 0000000..e3183da
--- /dev/null
+++ b/runtime/tests/vm/dart_2/scavenger_abort_2_test.dart
@@ -0,0 +1,288 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import "dart:io";
+import "package:expect/expect.dart";
+
+// The sizes of these classes are co-prime multiples of the allocation unit to
+// increase the likelihood that scavenging fails from fragmentation.
+
+// header + 13 fields = 7 allocation units
+class A {
+ dynamic field1;
+ dynamic field2;
+ dynamic field3;
+ dynamic field4;
+ dynamic field5;
+ dynamic field6;
+ dynamic field7;
+ dynamic field8;
+ dynamic field9;
+ dynamic field10;
+ dynamic field11;
+ dynamic field12;
+ dynamic field13;
+
+ // Prevent fields from being optimized away as write-only.
+ String toString() {
+ return field1 +
+ field2 +
+ field3 +
+ field4 +
+ field5 +
+ field6 +
+ field7 +
+ field8 +
+ field9 +
+ field10 +
+ field11 +
+ field12 +
+ field13;
+ }
+}
+
+// header + 17 fields = 9 allocation units
+class B {
+ dynamic field1;
+ dynamic field2;
+ dynamic field3;
+ dynamic field4;
+ dynamic field5;
+ dynamic field6;
+ dynamic field7;
+ dynamic field8;
+ dynamic field9;
+ dynamic field10;
+ dynamic field11;
+ dynamic field12;
+ dynamic field13;
+ dynamic field14;
+ dynamic field15;
+ dynamic field16;
+ dynamic field17;
+
+ // Prevent fields from being optimized away as write-only.
+ String toString() {
+ return field1 +
+ field2 +
+ field3 +
+ field4 +
+ field5 +
+ field6 +
+ field7 +
+ field8 +
+ field9 +
+ field10 +
+ field11 +
+ field12 +
+ field13 +
+ field14 +
+ field15 +
+ field16 +
+ field17;
+ }
+}
+
+// header + 19 fields = 10 allocation units
+class C {
+ dynamic field1;
+ dynamic field2;
+ dynamic field3;
+ dynamic field4;
+ dynamic field5;
+ dynamic field6;
+ dynamic field7;
+ dynamic field8;
+ dynamic field9;
+ dynamic field10;
+ dynamic field11;
+ dynamic field12;
+ dynamic field13;
+ dynamic field14;
+ dynamic field15;
+ dynamic field16;
+ dynamic field17;
+ dynamic field18;
+ dynamic field19;
+
+ // Prevent fields from being optimized away as write-only.
+ String toString() {
+ return field1 +
+ field2 +
+ field3 +
+ field4 +
+ field5 +
+ field6 +
+ field7 +
+ field8 +
+ field9 +
+ field10 +
+ field11 +
+ field12 +
+ field13 +
+ field14 +
+ field15 +
+ field16 +
+ field17 +
+ field18 +
+ field19;
+ }
+}
+
+class Old {
+ dynamic next;
+ dynamic new1;
+ dynamic new2;
+ dynamic new3;
+ dynamic new4;
+ dynamic new5;
+ dynamic new6;
+ dynamic new7;
+ dynamic new8;
+ dynamic new9;
+ dynamic new10;
+ dynamic new11;
+ dynamic new12;
+ dynamic new13;
+ dynamic new14;
+ dynamic new15;
+ dynamic new16;
+ dynamic new17;
+ dynamic new18;
+ dynamic new19;
+ dynamic new20;
+ dynamic new21;
+ dynamic new22;
+ dynamic new23;
+ dynamic new24;
+ dynamic new25;
+ dynamic new26;
+ dynamic new27;
+ dynamic new28;
+ dynamic new29;
+ dynamic new30;
+ dynamic new31;
+
+ // Prevent fields from being optimized away as write-only.
+ String toString() {
+ return new1 +
+ new2 +
+ new3 +
+ new4 +
+ new5 +
+ new6 +
+ new7 +
+ new8 +
+ new9 +
+ new10 +
+ new11 +
+ new12 +
+ new13 +
+ new14 +
+ new15 +
+ new16 +
+ new17 +
+ new18 +
+ new19 +
+ new20 +
+ new21 +
+ new22 +
+ new23 +
+ new24 +
+ new25 +
+ new26 +
+ new27 +
+ new28 +
+ new29 +
+ new30 +
+ new31;
+ }
+}
+
+fill(old) {
+ // Note the allocation order is different from the field order. The objects
+ // will be scavenged in field order, causing the objects to be rearranged to
+ // produce new-space fragmentation.
+ old.new1 = new C();
+ old.new4 = new C();
+ old.new7 = new C();
+ old.new10 = new C();
+ old.new13 = new C();
+ old.new16 = new C();
+
+ old.new2 = new B();
+ old.new5 = new B();
+ old.new8 = new B();
+ old.new11 = new B();
+ old.new14 = new B();
+ old.new17 = new B();
+
+ old.new3 = new A();
+ old.new6 = new A();
+ old.new9 = new A();
+ old.new12 = new A();
+ old.new15 = new A();
+ old.new18 = new A();
+}
+
+makeOld() {
+ // 2/4 MB worth of Old.
+ print("PHASE1");
+ var head;
+ for (var i = 0; i < 16384; i++) {
+ var old = new Old();
+ old.next = head;
+ head = old;
+ }
+
+ // 32/64 MB worth of new objects, all directly reachable from the
+ // remembered set.
+ print("PHASE2");
+ for (var old = head; old != null; old = old.next) {
+ fill(old);
+ }
+
+ print("PHASE3");
+ return head;
+}
+
+main(List<String> argsIn) async {
+ if (argsIn.contains("--testee")) {
+ // Trigger OOM.
+ // Must read the fields to prevent the writes from being optimized away. If
+ // the writes are optimized away, most of the tree is collectible right away
+ // and we timeout instead of triggering OOM.
+ print(makeOld());
+ return;
+ }
+
+ var exec = Platform.executable;
+ var args = Platform.executableArguments +
+ [
+ "--new_gen_semi_max_size=4" /*MB*/,
+ "--old_gen_heap_size=15" /*MB*/,
+ "--verbose_gc",
+ "--verify_store_buffer",
+ Platform.script.toFilePath(),
+ "--testee"
+ ];
+ print("+ $exec ${args.join(' ')}");
+
+ var result = await Process.run(exec, args);
+ print("Command stdout:");
+ print(result.stdout);
+ print("Command stderr:");
+ print(result.stderr);
+
+ Expect.equals(255, result.exitCode,
+ "Should see runtime exception error code, not SEGV");
+
+ Expect.isTrue(
+ result.stderr.contains("Unhandled exception:\nOut of Memory") ||
+ result.stderr.contains("Unhandled exception:\r\nOut of Memory"),
+ "Should see the Dart OutOfMemoryError");
+
+ Expect.isFalse(result.stderr.contains("error: Out of memory"),
+ "Should not see the C++ OUT_OF_MEMORY()");
+}
diff --git a/runtime/vm/heap/become.cc b/runtime/vm/heap/become.cc
index 8b9ac83..77cc4e5 100644
--- a/runtime/vm/heap/become.cc
+++ b/runtime/vm/heap/become.cc
@@ -86,25 +86,35 @@
virtual void VisitPointers(ObjectPtr* first, ObjectPtr* last) {
for (ObjectPtr* p = first; p <= last; p++) {
ObjectPtr old_target = *p;
+ ObjectPtr new_target;
if (IsForwardingObject(old_target)) {
- ObjectPtr new_target = GetForwardedObject(old_target);
- if (visiting_object_ == nullptr) {
- *p = new_target;
- } else if (visiting_object_->ptr()->IsCardRemembered()) {
- visiting_object_->ptr()->StoreArrayPointer(p, new_target, thread_);
- } else {
- visiting_object_->ptr()->StorePointer(p, new_target, thread_);
- }
+ new_target = GetForwardedObject(old_target);
+ } else {
+ // Though we do not need to update the slot's value when it is not
+ // forwarded, we do need to recheck the generational barrier. In
+ // particular, the remembered bit may be incorrectly false if this
+ // become was the result of aborting a scavenge while visiting the
+ // remembered set.
+ new_target = old_target;
+ }
+ if (visiting_object_ == nullptr) {
+ *p = new_target;
+ } else if (visiting_object_->ptr()->IsCardRemembered()) {
+ visiting_object_->ptr()->StoreArrayPointer(p, new_target, thread_);
+ } else {
+ visiting_object_->ptr()->StorePointer(p, new_target, thread_);
}
}
}
void VisitingObject(ObjectPtr obj) {
visiting_object_ = obj;
+ // The incoming remembered bit may be unreliable. Clear it so we can
+ // consistently reapply the barrier to all slots.
if ((obj != nullptr) && obj->IsOldObject() && obj->ptr()->IsRemembered()) {
ASSERT(!obj->IsForwardingCorpse());
ASSERT(!obj->IsFreeListElement());
- thread_->StoreBufferAddObjectGC(obj);
+ obj->ptr()->ClearRememberedBit();
}
}
diff --git a/runtime/vm/heap/scavenger.cc b/runtime/vm/heap/scavenger.cc
index e45ebd5..0de6a9f 100644
--- a/runtime/vm/heap/scavenger.cc
+++ b/runtime/vm/heap/scavenger.cc
@@ -830,7 +830,7 @@
if (raw_obj->IsHeapObject() && raw_obj->IsNewObject()) {
if (!is_remembered_) {
FATAL3(
- "Old object %#" Px "references new object %#" Px
+ "Old object %#" Px " references new object %#" Px
", but it is not"
" in any store buffer. Consider using rr to watch the slot %p and"
" reverse-continue to find the store with a missing barrier.\n",