[VM/AOT] Fixes incorrect deserialization of catch entry moves

The catch entry moves are serialized in a space-efficient manner using a
suffix tree.  The deserialization logic was not always correctly reading
the right catch entry moves which can cause incorrect stack frame on
catch entries and therefore can cause crashes.

Change-Id: Ic0eda8924f80262f5477ed5606eb6f74c55242c3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/96104
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Vyacheslav Egorov <vegorov@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
diff --git a/runtime/vm/catch_entry_moves_test.cc b/runtime/vm/catch_entry_moves_test.cc
new file mode 100644
index 0000000..faa41f6
--- /dev/null
+++ b/runtime/vm/catch_entry_moves_test.cc
@@ -0,0 +1,198 @@
+// Copyright (c) 2019, 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.
+
+#include <functional>
+#include <utility>
+
+#include "platform/assert.h"
+#include "vm/code_descriptors.h"
+#include "vm/exceptions.h"
+#include "vm/unit_test.h"
+
+namespace dart {
+
+static CatchEntryMove NewMove(intptr_t src, intptr_t dst) {
+  return CatchEntryMove::FromSlot(CatchEntryMove::SourceKind::kTaggedSlot, src,
+                                  dst);
+}
+const auto kA = NewMove(1, 10);
+const auto kB = NewMove(2, 20);
+const auto kC = NewMove(3, 30);
+const auto kD = NewMove(4, 40);
+const auto kE = NewMove(5, 50);
+const auto kX = NewMove(-1, -10);
+
+const CatchEntryMove abcde[] = {kA, kB, kC, kD, kE};
+const CatchEntryMove abcdx[] = {kA, kB, kC, kD, kX};
+const CatchEntryMove xbcde[] = {kX, kB, kC, kD, kE};
+const CatchEntryMove abxde[] = {kA, kB, kX, kD, kE};
+const CatchEntryMove ab[] = {kA, kB};
+const CatchEntryMove de[] = {kD, kE};
+
+struct TestCaseMoves {
+  const CatchEntryMove* moves;
+  const intptr_t count;
+};
+
+void RunTestCaseWithPermutations(const TestCaseMoves* mapping,
+                                 intptr_t* insert_permutation,
+                                 intptr_t count) {
+  CatchEntryMovesMapBuilder b;
+
+  for (intptr_t i = 0; i < count; ++i) {
+    auto expected_moves = mapping[insert_permutation[i]];
+    b.NewMapping(/*pc_offset=*/insert_permutation[i]);
+    for (intptr_t j = 0; j < expected_moves.count; ++j) {
+      b.Append(expected_moves.moves[j]);
+    }
+    b.EndMapping();
+  }
+
+  const auto& bytes = TypedData::Handle(b.FinalizeCatchEntryMovesMap());
+  CatchEntryMovesMapReader reader(bytes);
+
+  for (intptr_t i = 0; i < count; ++i) {
+    auto expected_moves = mapping[i];
+    auto read_moves = reader.ReadMovesForPcOffset(i);
+    EXPECT_EQ(expected_moves.count, read_moves->count());
+    for (intptr_t j = 0; j < expected_moves.count; ++j) {
+      EXPECT(expected_moves.moves[j] == read_moves->At(j));
+    }
+    free(read_moves);
+  }
+}
+
+void RunTestCase(const TestCaseMoves* mapping, intptr_t count) {
+  std::unique_ptr<intptr_t[]> permutation(new intptr_t[count]);
+  for (intptr_t i = 0; i < count; ++i) {
+    permutation[i] = i;
+  }
+
+  std::function<void(intptr_t)> run_all_permutations = [&](intptr_t offset) {
+    if (offset == count) {
+      RunTestCaseWithPermutations(mapping, &permutation[0], count);
+    } else {
+      for (intptr_t i = offset; i < count; ++i) {
+        const intptr_t start = permutation[offset];
+        const intptr_t replacement = permutation[i];
+
+        permutation[offset] = replacement;
+        permutation[i] = start;
+
+        run_all_permutations(offset + 1);
+
+        permutation[offset] = start;
+        permutation[i] = replacement;
+      }
+    }
+  };
+
+  run_all_permutations(0);
+}
+
+ISOLATE_UNIT_TEST_CASE(CatchEntryMoves) {
+  // Common prefix.
+  const TestCaseMoves test1[] = {
+      TestCaseMoves{
+          abcde,
+          ARRAY_SIZE(abcde),
+      },
+      TestCaseMoves{
+          abcdx,
+          ARRAY_SIZE(abcdx),
+      },
+  };
+  RunTestCase(test1, ARRAY_SIZE(test1));
+
+  // Common suffix.
+  const TestCaseMoves test2[] = {
+      TestCaseMoves{
+          abcde,
+          ARRAY_SIZE(abcde),
+      },
+      TestCaseMoves{
+          xbcde,
+          ARRAY_SIZE(xbcde),
+      },
+  };
+  RunTestCase(test2, ARRAY_SIZE(test2));
+
+  // Common prefix and suffix.
+  const TestCaseMoves test3[] = {
+      TestCaseMoves{
+          abcde,
+          ARRAY_SIZE(abcde),
+      },
+      TestCaseMoves{
+          abxde,
+          ARRAY_SIZE(abxde),
+      },
+  };
+  RunTestCase(test3, ARRAY_SIZE(test3));
+
+  // Subset of suffix.
+  const TestCaseMoves test4[] = {
+      TestCaseMoves{
+          abcde,
+          ARRAY_SIZE(abcde),
+      },
+      TestCaseMoves{
+          de,
+          ARRAY_SIZE(de),
+      },
+  };
+  RunTestCase(test4, ARRAY_SIZE(test4));
+
+  // Subset of prefix.
+  const TestCaseMoves test5[] = {
+      TestCaseMoves{
+          abcde,
+          ARRAY_SIZE(abcde),
+      },
+      TestCaseMoves{
+          ab,
+          ARRAY_SIZE(ab),
+      },
+  };
+  RunTestCase(test5, ARRAY_SIZE(test5));
+
+  // All moves (with duplicates).
+  const TestCaseMoves test6[] = {
+      TestCaseMoves{
+          abcde,
+          ARRAY_SIZE(abcde),
+      },
+      TestCaseMoves{
+          abcde,
+          ARRAY_SIZE(abcde),
+      },
+      TestCaseMoves{
+          abcdx,
+          ARRAY_SIZE(abcdx),
+      },
+      TestCaseMoves{
+          xbcde,
+          ARRAY_SIZE(xbcde),
+      },
+      TestCaseMoves{
+          abxde,
+          ARRAY_SIZE(abxde),
+      },
+      TestCaseMoves{
+          ab,
+          ARRAY_SIZE(ab),
+      },
+      TestCaseMoves{
+          de,
+          ARRAY_SIZE(de),
+      },
+      TestCaseMoves{
+          de,
+          ARRAY_SIZE(de),
+      },
+  };
+  RunTestCase(test6, ARRAY_SIZE(test6));
+}
+
+}  // namespace dart
diff --git a/runtime/vm/exceptions.cc b/runtime/vm/exceptions.cc
index 487b22a..d310c00 100644
--- a/runtime/vm/exceptions.cc
+++ b/runtime/vm/exceptions.cc
@@ -301,58 +301,12 @@
 
 #if defined(DART_PRECOMPILED_RUNTIME) || defined(DART_PRECOMPILER)
   void ReadCompressedCatchEntryMoves() {
-    intptr_t pc_offset = pc_ - code_->PayloadStart();
-    const TypedData& td = TypedData::Handle(code_->catch_entry_moves_maps());
-    NoSafepointScope no_safepoint;
-    ReadStream stream(static_cast<uint8_t*>(td.DataAddr(0)), td.Length());
+    const intptr_t pc_offset = pc_ - code_->PayloadStart();
+    const auto& td = TypedData::Handle(code_->catch_entry_moves_maps());
 
-    intptr_t prefix_length = 0, suffix_length = 0, suffix_offset = 0;
-    while (stream.PendingBytes() > 0) {
-      intptr_t target_pc_offset = Reader::Read(&stream);
-      prefix_length = Reader::Read(&stream);
-      suffix_length = Reader::Read(&stream);
-      suffix_offset = Reader::Read(&stream);
-      if (pc_offset == target_pc_offset) {
-        break;
-      }
-
-      // Skip the moves.
-      for (intptr_t j = 0; j < prefix_length; j++) {
-        CatchEntryMove::ReadFrom(&stream);
-      }
-    }
-    ASSERT((stream.PendingBytes() > 0) || (prefix_length == 0));
-
-    CatchEntryMoves* moves =
-        CatchEntryMoves::Allocate(prefix_length + suffix_length);
-    for (int j = 0; j < prefix_length; j++) {
-      moves->At(j) = CatchEntryMove::ReadFrom(&stream);
-    }
-    ReadCompressedCatchEntryMovesSuffix(&stream, suffix_offset, suffix_length,
-                                        moves, prefix_length);
-    catch_entry_moves_ = moves;
+    CatchEntryMovesMapReader reader(td);
+    catch_entry_moves_ = reader.ReadMovesForPcOffset(pc_offset);
   }
-
-  void ReadCompressedCatchEntryMovesSuffix(ReadStream* stream,
-                                           intptr_t offset,
-                                           intptr_t length,
-                                           CatchEntryMoves* moves,
-                                           intptr_t moves_offset) {
-    stream->SetPosition(offset);
-    Reader::Read(stream);  // skip pc_offset
-    Reader::Read(stream);  // skip variables
-    intptr_t suffix_length = Reader::Read(stream);
-    intptr_t suffix_offset = Reader::Read(stream);
-    intptr_t to_read = length - suffix_length;
-    for (int j = 0; j < to_read; j++) {
-      moves->At(moves_offset + j) = CatchEntryMove::ReadFrom(stream);
-    }
-    if (suffix_length > 0) {
-      ReadCompressedCatchEntryMovesSuffix(stream, suffix_offset, suffix_length,
-                                          moves, moves_offset + to_read);
-    }
-  }
-
 #else
   void GetCatchEntryMovesFromDeopt(intptr_t num_vars, StackFrame* frame) {
     Isolate* isolate = thread_->isolate();
@@ -415,6 +369,79 @@
 }
 #endif
 
+CatchEntryMoves* CatchEntryMovesMapReader::ReadMovesForPcOffset(
+    intptr_t pc_offset) {
+  NoSafepointScope no_safepoint;
+
+  ReadStream stream(static_cast<uint8_t*>(bytes_.DataAddr(0)), bytes_.Length());
+
+  intptr_t position = 0;
+  intptr_t length = 0;
+  FindEntryForPc(&stream, pc_offset, &position, &length);
+
+  return ReadCompressedCatchEntryMovesSuffix(&stream, position, length);
+}
+
+void CatchEntryMovesMapReader::FindEntryForPc(ReadStream* stream,
+                                              intptr_t pc_offset,
+                                              intptr_t* position,
+                                              intptr_t* length) {
+  using Reader = ReadStream::Raw<sizeof(intptr_t), intptr_t>;
+
+  while (stream->PendingBytes() > 0) {
+    const intptr_t stream_position = stream->Position();
+    const intptr_t target_pc_offset = Reader::Read(stream);
+    const intptr_t prefix_length = Reader::Read(stream);
+    const intptr_t suffix_length = Reader::Read(stream);
+    Reader::Read(stream);  // Skip suffix_offset
+    if (pc_offset == target_pc_offset) {
+      *position = stream_position;
+      *length = prefix_length + suffix_length;
+      return;
+    }
+
+    // Skip the prefix moves.
+    for (intptr_t j = 0; j < prefix_length; j++) {
+      CatchEntryMove::ReadFrom(stream);
+    }
+  }
+
+  UNREACHABLE();
+}
+
+CatchEntryMoves* CatchEntryMovesMapReader::ReadCompressedCatchEntryMovesSuffix(
+    ReadStream* stream,
+    intptr_t offset,
+    intptr_t length) {
+  using Reader = ReadStream::Raw<sizeof(intptr_t), intptr_t>;
+
+  CatchEntryMoves* moves = CatchEntryMoves::Allocate(length);
+
+  intptr_t remaining_length = length;
+
+  intptr_t moves_offset = 0;
+  while (remaining_length > 0) {
+    stream->SetPosition(offset);
+    Reader::Read(stream);  // skip pc_offset
+    Reader::Read(stream);  // skip prefix length
+    const intptr_t suffix_length = Reader::Read(stream);
+    const intptr_t suffix_offset = Reader::Read(stream);
+    const intptr_t to_read = remaining_length - suffix_length;
+    if (to_read > 0) {
+      for (int j = 0; j < to_read; j++) {
+        // The prefix is written from the back.
+        moves->At(moves_offset + to_read - j - 1) =
+            CatchEntryMove::ReadFrom(stream);
+      }
+      remaining_length -= to_read;
+      moves_offset += to_read;
+    }
+    offset = suffix_offset;
+  }
+
+  return moves;
+}
+
 static void FindErrorHandler(uword* handler_pc,
                              uword* handler_sp,
                              uword* handler_fp) {
diff --git a/runtime/vm/exceptions.h b/runtime/vm/exceptions.h
index d43201f..894f374 100644
--- a/runtime/vm/exceptions.h
+++ b/runtime/vm/exceptions.h
@@ -27,6 +27,7 @@
 class WriteStream;
 class String;
 class Thread;
+class TypedData;
 
 class Exceptions : AllStatic {
  public:
@@ -190,7 +191,7 @@
            (dest_slot() == src_slot());
   }
 
-  bool operator==(const CatchEntryMove& rhs) {
+  bool operator==(const CatchEntryMove& rhs) const {
     return src_ == rhs.src_ && dest_and_kind_ == rhs.dest_and_kind_;
   }
 
@@ -253,6 +254,30 @@
   // Followed by CatchEntryMove[count_]
 };
 
+// Used for reading the [CatchEntryMoves] from the compressed form.
+class CatchEntryMovesMapReader : public ValueObject {
+ public:
+  explicit CatchEntryMovesMapReader(const TypedData& bytes) : bytes_(bytes) {}
+
+  // The returned [CatchEntryMoves] must be freed by the caller via [free].
+  CatchEntryMoves* ReadMovesForPcOffset(intptr_t pc_offset);
+
+ private:
+  // Given the [pc_offset] this function will find the [position] at which to
+  // read the catch entries and the [length] of the catch entry moves array.
+  void FindEntryForPc(ReadStream* stream,
+                      intptr_t pc_offset,
+                      intptr_t* position,
+                      intptr_t* length);
+
+  // Reads the [length] catch entry moves from [offset] in the [stream].
+  CatchEntryMoves* ReadCompressedCatchEntryMovesSuffix(ReadStream* stream,
+                                                       intptr_t offset,
+                                                       intptr_t length);
+
+  const TypedData& bytes_;
+};
+
 // A simple reference counting wrapper for CatchEntryMoves.
 //
 // TODO(vegorov) switch this to intrusive reference counting.
diff --git a/runtime/vm/vm_sources.gni b/runtime/vm/vm_sources.gni
index 717b50c..4081d35 100644
--- a/runtime/vm/vm_sources.gni
+++ b/runtime/vm/vm_sources.gni
@@ -373,6 +373,7 @@
   "bitfield_test.cc",
   "bitmap_test.cc",
   "boolfield_test.cc",
+  "catch_entry_moves_test.cc",
   "class_finalizer_test.cc",
   "code_descriptors_test.cc",
   "code_patcher_arm64_test.cc",