Limit the maximum length of frame unique names.

The unique name is a semi-stable identifier used to identify the target
frame for back/forward and session restore. The original unique name
generation algorithm included the browsing context name: unfortunately,
certain sites use window.name to transport large amounts of data,
causing session restore data to balloon in size.

The original plan was to strictly limit the length of unique names;
however, ensuring backwards compatibility was complex and difficult to
understand. Instead, this patch enforces a weaker guarantee: if a frame
provides a hint for the unique name that is over 80 characters, hash
the requested name and use the result as if it were the requested name
instead. It's still possible to get fairly long names with deeply
nested frames, but this should be a large improvement over the current
situation with no limit at all.

Note that even the simpler version of this algorithm does not result in
perfect backwards compatibility: a malicious page can intentionally
pick browsing context names that only collide once the name is hashed.
Since this only affects the page itself, the algorithm retains the
current best effort collision avoidance strategy of picking a name that
is unlikely to collide, without guaranteeing full collision resistance.

Browsing a small assortment of control pages shows that unique name
length is reduced from an average of ~1260 characters to 70 characters.
Note that this metric was originally implemented incorrectly: for the
purpose of comparison, the new metric was recorded in the exact same
way. Actual numbers in the field are probably somewhat lower than this.

Bug: 626202, 645123
Cq-Include-Trybots: master.tryserver.chromium.linux:linux_site_isolation
Change-Id: I63c481feaf708c5e0d4087dafc8fcbf59b9091a6
Reviewed-on: https://chromium-review.googlesource.com/579031
Reviewed-by: Mark Pearson <[email protected]>
Reviewed-by: Charlie Reis <[email protected]>
Cr-Commit-Position: refs/heads/master@{#493153}
diff --git a/content/browser/frame_host/frame_tree_node.cc b/content/browser/frame_host/frame_tree_node.cc
index c4682bd5..c2f3a0ec 100644
--- a/content/browser/frame_host/frame_tree_node.cc
+++ b/content/browser/frame_host/frame_tree_node.cc
@@ -4,6 +4,8 @@
 
 #include "content/browser/frame_host/frame_tree_node.h"
 
+#include <math.h>
+
 #include <queue>
 #include <utility>
 
@@ -13,6 +15,7 @@
 #include "base/metrics/histogram_macros.h"
 #include "base/profiler/scoped_tracker.h"
 #include "base/stl_util.h"
+#include "base/strings/string_util.h"
 #include "content/browser/frame_host/frame_tree.h"
 #include "content/browser/frame_host/navigation_request.h"
 #include "content/browser/frame_host/navigator.h"
@@ -42,8 +45,44 @@
 const double kLoadingProgressMinimum = 0.1;
 const double kLoadingProgressDone = 1.0;
 
-void RecordUniqueNameLength(size_t length) {
-  UMA_HISTOGRAM_COUNTS("SessionRestore.FrameUniqueNameLength", length);
+void RecordUniqueNameSize(FrameTreeNode* node) {
+  const auto& unique_name = node->current_replication_state().unique_name;
+
+  // Don't record numbers for the root node, which always has an empty unique
+  // name.
+  if (!node->parent()) {
+    DCHECK(unique_name.empty());
+    return;
+  }
+
+  // The original requested name is derived from the browsing context name and
+  // is essentially unbounded in size...
+  UMA_HISTOGRAM_COUNTS_1M(
+      "SessionRestore.FrameUniqueNameOriginalRequestedNameSize",
+      node->current_replication_state().name.size());
+  // If the name is a frame path, attempt to normalize the statistics based on
+  // the number of frames in the frame path.
+  if (base::StartsWith(unique_name, "<!--framePath //",
+                       base::CompareCase::SENSITIVE)) {
+    size_t depth = 1;
+    while (node->parent()) {
+      ++depth;
+      node = node->parent();
+    }
+    // The max possible size of a unique name is 80 characters, so the expected
+    // size per component shouldn't be much more than that.
+    UMA_HISTOGRAM_COUNTS_100(
+        "SessionRestore.FrameUniqueNameWithFramePathSizePerComponent",
+        round(unique_name.size() / static_cast<float>(depth)));
+    // Blink allows a maximum of ~1024 subframes in a document, so this should
+    // be less than (80 character name + 1 character delimiter) * 1024.
+    UMA_HISTOGRAM_COUNTS_100000(
+        "SessionRestore.FrameUniqueNameWithFramePathSize", unique_name.size());
+  } else {
+    UMA_HISTOGRAM_COUNTS_100(
+        "SessionRestore.FrameUniqueNameFromRequestedNameSize",
+        unique_name.size());
+  }
 }
 
 }  // namespace
@@ -124,7 +163,7 @@
           std::make_pair(frame_tree_node_id_, this));
   CHECK(result.second);
 
-  RecordUniqueNameLength(unique_name.size());
+  RecordUniqueNameSize(this);
 
   // Note: this should always be done last in the constructor.
   blame_context_.Initialize();
@@ -286,7 +325,10 @@
     DCHECK(unique_name.empty());
   }
 
-  RecordUniqueNameLength(unique_name.size());
+  // Note the unique name should only be able to change before the first real
+  // load is committed, but that's not strongly enforced here.
+  if (unique_name != replication_state_.unique_name)
+    RecordUniqueNameSize(this);
   render_manager_.OnDidUpdateName(name, unique_name);
   replication_state_.name = name;
   replication_state_.unique_name = unique_name;
diff --git a/content/common/page_state_serialization.cc b/content/common/page_state_serialization.cc
index c4289a5..412aa43a 100644
--- a/content/common/page_state_serialization.cc
+++ b/content/common/page_state_serialization.cc
@@ -15,6 +15,7 @@
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
+#include "content/common/unique_name_helper.h"
 #include "content/public/common/resource_request_body.h"
 #include "ui/display/display.h"
 #include "ui/display/screen.h"
@@ -197,12 +198,13 @@
 // 22: Add scroll restoration type.
 // 23: Remove frame sequence number, there are easier ways.
 // 24: Add did save scroll or scale state.
+// 25: Limit the length of unique names: https://crbug.com/626202
 //
 // NOTE: If the version is -1, then the pickle contains only a URL string.
 // See ReadPageState.
 //
 const int kMinVersion = 11;
-const int kCurrentVersion = 24;
+const int kCurrentVersion = 25;
 
 // A bunch of convenience functions to read/write to SerializeObjects.  The
 // de-serializers assume the input data will be in the correct format and fall
@@ -550,8 +552,11 @@
     WriteFrameState(children[i], obj, false);
 }
 
-void ReadFrameState(SerializeObject* obj, bool is_top,
-                    ExplodedFrameState* state) {
+void ReadFrameState(
+    SerializeObject* obj,
+    bool is_top,
+    std::vector<UniqueNameHelper::Replacement>* unique_name_replacements,
+    ExplodedFrameState* state) {
   if (obj->version < 14 && !is_top)
     ReadInteger(obj);  // Skip over redundant version field.
 
@@ -561,6 +566,13 @@
     ReadString(obj);  // Skip obsolete original url string field.
 
   state->target = ReadString(obj);
+  if (obj->version < 25 && !state->target.is_null()) {
+    state->target = base::NullableString16(
+        base::UTF8ToUTF16(UniqueNameHelper::UpdateLegacyNameFromV24(
+            base::UTF16ToUTF8(state->target.string()),
+            unique_name_replacements)),
+        false);
+  }
   if (obj->version < 15) {
     ReadString(obj);  // Skip obsolete parent field.
     ReadString(obj);  // Skip obsolete title field.
@@ -660,7 +672,7 @@
       ReadAndValidateVectorSize(obj, sizeof(ExplodedFrameState));
   state->children.resize(num_children);
   for (size_t i = 0; i < num_children; ++i)
-    ReadFrameState(obj, false, &state->children[i]);
+    ReadFrameState(obj, false, unique_name_replacements, &state->children[i]);
 }
 
 void WritePageState(const ExplodedPageState& state, SerializeObject* obj) {
@@ -689,7 +701,8 @@
   if (obj->version >= 14)
     ReadStringVector(obj, &state->referenced_files);
 
-  ReadFrameState(obj, true, &state->top);
+  std::vector<UniqueNameHelper::Replacement> unique_name_replacements;
+  ReadFrameState(obj, true, &unique_name_replacements, &state->top);
 
   if (obj->version < 14)
     RecursivelyAppendReferencedFiles(state->top, &state->referenced_files);
@@ -763,13 +776,25 @@
   return !obj.parse_error;
 }
 
-void EncodePageState(const ExplodedPageState& exploded, std::string* encoded) {
+static void EncodePageStateInternal(const ExplodedPageState& exploded,
+                                    int version,
+                                    std::string* encoded) {
   SerializeObject obj;
-  obj.version = kCurrentVersion;
+  obj.version = version;
   WritePageState(exploded, &obj);
   *encoded = obj.GetAsString();
 }
 
+void EncodePageState(const ExplodedPageState& exploded, std::string* encoded) {
+  EncodePageStateInternal(exploded, kCurrentVersion, encoded);
+}
+
+void EncodePageStateForTesting(const ExplodedPageState& exploded,
+                               int version,
+                               std::string* encoded) {
+  EncodePageStateInternal(exploded, version, encoded);
+}
+
 #if defined(OS_ANDROID)
 bool DecodePageStateWithDeviceScaleFactorForTesting(
     const std::string& encoded,
diff --git a/content/common/page_state_serialization.h b/content/common/page_state_serialization.h
index d7a0bf3..808a421 100644
--- a/content/common/page_state_serialization.h
+++ b/content/common/page_state_serialization.h
@@ -74,6 +74,9 @@
                                     ExplodedPageState* exploded);
 CONTENT_EXPORT void EncodePageState(const ExplodedPageState& exploded,
                                     std::string* encoded);
+CONTENT_EXPORT void EncodePageStateForTesting(const ExplodedPageState& exploded,
+                                              int version,
+                                              std::string* encoded);
 
 #if defined(OS_ANDROID)
 CONTENT_EXPORT bool DecodePageStateWithDeviceScaleFactorForTesting(
diff --git a/content/common/page_state_serialization_unittest.cc b/content/common/page_state_serialization_unittest.cc
index bd44937..25375870 100644
--- a/content/common/page_state_serialization_unittest.cc
+++ b/content/common/page_state_serialization_unittest.cc
@@ -443,5 +443,9 @@
   TestBackwardsCompat(23);
 }
 
+TEST_F(PageStateSerializationTest, BackwardsCompat_v24) {
+  TestBackwardsCompat(24);
+}
+
 }  // namespace
 }  // namespace content
diff --git a/content/common/unique_name_helper.cc b/content/common/unique_name_helper.cc
index a6a135b..20992fc0 100644
--- a/content/common/unique_name_helper.cc
+++ b/content/common/unique_name_helper.cc
@@ -5,10 +5,13 @@
 #include "content/common/unique_name_helper.h"
 
 #include <algorithm>
+#include <utility>
 
 #include "base/logging.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/strings/string_piece.h"
 #include "base/strings/string_util.h"
+#include "crypto/sha2.h"
 
 namespace content {
 
@@ -22,7 +25,7 @@
 
   // FrameAdapter overrides:
   bool IsMainFrame() const override { return false; }
-  bool IsCandidateUnique(const std::string& name) const override {
+  bool IsCandidateUnique(base::StringPiece name) const override {
     return parent_->IsCandidateUnique(name);
   }
   int GetSiblingCount() const override {
@@ -54,6 +57,11 @@
 constexpr int kFramePathPrefixLength = 15;
 constexpr int kFramePathSuffixLength = 3;
 
+// 80% of unique names are shorter than this, and it also guarantees that this
+// won't ever increase the length of a unique name, as a hashed unique name is
+// exactly 80 characters.
+constexpr size_t kMaxRequestedNameSize = 80;
+
 bool IsNameWithFramePath(base::StringPiece name) {
   return name.starts_with(kFramePathPrefix) && name.ends_with("-->") &&
          (kFramePathPrefixLength + kFramePathSuffixLength) < name.size();
@@ -130,10 +138,10 @@
   return candidate;
 }
 
-std::string CalculateNewName(const FrameAdapter* frame,
-                             const std::string& name) {
+std::string CalculateNameInternal(const FrameAdapter* frame,
+                                  base::StringPiece name) {
   if (!name.empty() && frame->IsCandidateUnique(name) && name != "_blank")
-    return name;
+    return name.as_string();
 
   std::string candidate = GenerateCandidate(frame);
   if (frame->IsCandidateUnique(candidate))
@@ -143,10 +151,39 @@
   return AppendUniqueSuffix(frame, candidate, likely_unique_suffix);
 }
 
+std::string CalculateFrameHash(base::StringPiece name) {
+  DCHECK_GT(name.size(), kMaxRequestedNameSize);
+
+  std::string hashed_name;
+  uint8_t result[crypto::kSHA256Length];
+  crypto::SHA256HashString(name, result, arraysize(result));
+  hashed_name += "<!--frameHash";
+  hashed_name += base::HexEncode(result, arraysize(result));
+  hashed_name += "-->";
+  return hashed_name;
+}
+
+std::string CalculateNewName(const FrameAdapter* frame,
+                             base::StringPiece name) {
+  std::string hashed_name;
+  // By default, |name| is the browsing context name, which can be arbitrarily
+  // long. Since the generated name is part of history entries and FrameState,
+  // hash pathologically long names to avoid using a lot of memory.
+  if (name.size() > kMaxRequestedNameSize) {
+    hashed_name = CalculateFrameHash(name);
+    name = hashed_name;
+  }
+  return CalculateNameInternal(frame, name);
+}
+
 }  // namespace
 
 UniqueNameHelper::FrameAdapter::~FrameAdapter() {}
 
+UniqueNameHelper::Replacement::Replacement(std::string old_name,
+                                           std::string new_name)
+    : old_name(std::move(old_name)), new_name(std::move(new_name)) {}
+
 UniqueNameHelper::UniqueNameHelper(FrameAdapter* frame) : frame_(frame) {}
 
 UniqueNameHelper::~UniqueNameHelper() {}
@@ -167,4 +204,74 @@
   unique_name_ = CalculateNewName(frame_, name);
 }
 
+// |replacements| is used for two purposes:
+// - when processing a non-frame path unique name that exceeds the max size,
+//   this collection records the original name and the hashed name.
+// - when processing a frame path unique name, this collection is used to fix up
+//   ancestor frames in the frame path with an updated unique name.
+//
+std::string UniqueNameHelper::UpdateLegacyNameFromV24(
+    std::string legacy_name,
+    std::vector<Replacement>* replacements) {
+  if (IsNameWithFramePath(legacy_name)) {
+    // Frame paths can embed ancestor's unique names. Since the contract of this
+    // function is that names must be updated beginning from the root of the
+    // tree and go down from there, it is impossible for a frame path to contain
+    // a unique name (which needs a replacement) that has not already been seen
+    // and inserted into |replacements|.
+    size_t index = 0;
+    for (const auto& replacement : *replacements) {
+      size_t next_index = legacy_name.find(replacement.old_name);
+      if (next_index == std::string::npos)
+        continue;
+      legacy_name.replace(next_index, replacement.old_name.size(),
+                          replacement.new_name);
+      index = next_index -
+              (replacement.old_name.size() - replacement.new_name.size());
+    }
+    return legacy_name;
+  }
+
+  if (legacy_name.size() > kMaxRequestedNameSize) {
+    std::string hashed_name = CalculateFrameHash(legacy_name);
+    // Suppose 'aaa' and 'caaab' are unique names in the same tree. A
+    // hypothetical frame path might look like:
+    //   <!--framePath //aaa/caaab/<!--frame0-->-->
+    //
+    // In this case, it's important to avoid matching 'aaa' against the
+    // substring in 'caaab'. To try to avoid this, the search and the
+    // replacement strings are wrapped in '/' to try to match the path delimiter
+    // in generated frame paths.
+    //
+    // However, nothing prevents a browsing context name from containing a
+    // literal '/', which could lead to an ambiguous parse. Consider the case
+    // where 'aaa', 'bbb', and 'aaa/bbb' are unique names in the same tree. The
+    // following frame path is ambiguous:
+    //   <!--framePath //aaa/bbb/<!--frame0-->-->
+    //
+    // While it's possible to use the depth of the frame tree as a hint for
+    // disambiguating this, the number of ways to split up the frame path
+    // quickly becomes quite large. This code takes the simple approach and
+    // simply aims to implement a best effort update, accepting that there may
+    // be some names that are updated incorrectly.
+    std::string original_string = "/";
+    original_string += legacy_name;
+    original_string += "/";
+    std::string new_string = "/";
+    new_string += hashed_name;
+    new_string += "/";
+    replacements->emplace_back(std::move(original_string),
+                               std::move(new_string));
+    return hashed_name;
+  }
+
+  return legacy_name;
+}
+
+std::string UniqueNameHelper::CalculateLegacyNameForTesting(
+    const FrameAdapter* frame,
+    const std::string& name) {
+  return CalculateNameInternal(frame, name);
+}
+
 }  // namespace content
diff --git a/content/common/unique_name_helper.h b/content/common/unique_name_helper.h
index dccea5c..d816305 100644
--- a/content/common/unique_name_helper.h
+++ b/content/common/unique_name_helper.h
@@ -10,6 +10,7 @@
 
 #include "base/macros.h"
 #include "base/strings/string_piece.h"
+#include "content/common/content_export.h"
 
 namespace content {
 
@@ -64,17 +65,17 @@
 //                   [ framePosition-forParent? ]
 //
 // retryNumber ::= smallest non-negative integer resulting in unique name
-class UniqueNameHelper {
+class CONTENT_EXPORT UniqueNameHelper {
  public:
   // Adapter class so UniqueNameHelper can be used with both RenderFrameImpl and
   // ExplodedFrameState.
-  class FrameAdapter {
+  class CONTENT_EXPORT FrameAdapter {
    public:
     FrameAdapter() {}
     virtual ~FrameAdapter();
 
     virtual bool IsMainFrame() const = 0;
-    virtual bool IsCandidateUnique(const std::string& name) const = 0;
+    virtual bool IsCandidateUnique(base::StringPiece name) const = 0;
     // Returns the number of sibling frames of this frame. Note this should not
     // include this frame in the count.
     virtual int GetSiblingCount() const = 0;
@@ -106,6 +107,13 @@
     DISALLOW_COPY_AND_ASSIGN(FrameAdapter);
   };
 
+  struct Replacement {
+    Replacement(std::string old_name, std::string new_name);
+
+    const std::string old_name;
+    const std::string new_name;
+  };
+
   explicit UniqueNameHelper(FrameAdapter* frame);
   ~UniqueNameHelper();
 
@@ -134,6 +142,19 @@
   // history navigations. See https://crbug.com/607205.
   void UpdateName(const std::string& name);
 
+  // Helper to update legacy names generated for PageState v24 and earlier. This
+  // function should be invoked starting from the root of the tree, traversing
+  // downwards. The exact traversal order is unimportant as long as this
+  // function has been called on all ancestor frames of the node associated with
+  // |legacy_name|. A single instance of |replacements| should be used per frame
+  // tree.
+  static std::string UpdateLegacyNameFromV24(
+      std::string legacy_name,
+      std::vector<Replacement>* replacements);
+
+  static std::string CalculateLegacyNameForTesting(const FrameAdapter* frame,
+                                                   const std::string& name);
+
  private:
   FrameAdapter* const frame_;
   std::string unique_name_;
diff --git a/content/common/unique_name_helper_unittest.cc b/content/common/unique_name_helper_unittest.cc
new file mode 100644
index 0000000..ddf240d4
--- /dev/null
+++ b/content/common/unique_name_helper_unittest.cc
@@ -0,0 +1,411 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/common/unique_name_helper.h"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+#include "base/auto_reset.h"
+#include "base/memory/ptr_util.h"
+#include "base/optional.h"
+#include "base/strings/nullable_string16.h"
+#include "base/strings/string_piece.h"
+#include "base/strings/utf_string_conversions.h"
+#include "content/common/page_state_serialization.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+namespace {
+
+// Requested names longer than this (that are unique) should be hashed.
+constexpr size_t kMaxSize = 80;
+
+class TestFrameAdapter : public UniqueNameHelper::FrameAdapter {
+ public:
+  // |virtual_index_in_parent| is the virtual index of this frame in the
+  // parent's list of children, as unique name generation should see it. Note
+  // that this may differ from the actual index of this adapter in
+  // |parent_->children_|.
+  explicit TestFrameAdapter(TestFrameAdapter* parent,
+                            int virtual_index_in_parent,
+                            const std::string& requested_name)
+      : parent_(parent),
+        virtual_index_in_parent_(virtual_index_in_parent),
+        unique_name_helper_(this) {
+    if (parent_)
+      parent_->children_.push_back(this);
+    unique_name_helper_.UpdateName(requested_name);
+    CalculateLegacyName(requested_name);
+  }
+
+  ~TestFrameAdapter() override {
+    if (parent_) {
+      parent_->children_.erase(std::find(parent_->children_.begin(),
+                                         parent_->children_.end(), this));
+    }
+  }
+
+  bool IsMainFrame() const override { return !parent_; }
+
+  bool IsCandidateUnique(base::StringPiece name) const override {
+    auto* top = this;
+    while (top->parent_)
+      top = top->parent_;
+    return top->CheckUniqueness(name);
+  }
+
+  int GetSiblingCount() const override { return virtual_index_in_parent_; }
+
+  int GetChildCount() const override {
+    ADD_FAILURE()
+        << "GetChildCount() should not be triggered by unit test code!";
+    return 0;
+  }
+
+  std::vector<base::StringPiece> CollectAncestorNames(
+      BeginPoint begin_point,
+      bool (*should_stop)(base::StringPiece)) const override {
+    EXPECT_EQ(BeginPoint::kParentFrame, begin_point);
+    std::vector<base::StringPiece> result;
+    for (auto* adapter = parent_; adapter; adapter = adapter->parent_) {
+      result.push_back(adapter->GetNameForCurrentMode());
+      if (should_stop(result.back()))
+        break;
+    }
+    return result;
+  }
+
+  std::vector<int> GetFramePosition(BeginPoint begin_point) const override {
+    EXPECT_EQ(BeginPoint::kParentFrame, begin_point);
+    std::vector<int> result;
+    for (auto* adapter = this; adapter->parent_; adapter = adapter->parent_)
+      result.push_back(adapter->virtual_index_in_parent_);
+    return result;
+  }
+
+  // Returns the new style name with a max size limit.
+  const std::string& GetUniqueName() const {
+    return unique_name_helper_.value();
+  }
+
+  // Calculate and return the legacy style name with no max size limit.
+  const std::string& GetLegacyName() const { return legacy_name_; }
+
+  // Populate a tree of FrameState with legacy unique names. The order of
+  // FrameState children is guaranteed to match the order of TestFrameAdapter
+  // children.
+  void PopulateLegacyFrameState(ExplodedFrameState* frame_state) const {
+    frame_state->target =
+        base::NullableString16(base::UTF8ToUTF16(GetLegacyName()), false);
+    frame_state->children.resize(children_.size());
+    for (size_t i = 0; i < children_.size(); ++i)
+      children_[i]->PopulateLegacyFrameState(&frame_state->children[i]);
+  }
+
+  // Recursively verify that FrameState and its children have matching unique
+  // names to this TestFrameAdapter.
+  void VerifyUpdatedFrameState(const ExplodedFrameState& frame_state) const {
+    EXPECT_EQ(GetUniqueName(), base::UTF16ToUTF8(frame_state.target.string()));
+
+    ASSERT_EQ(children_.size(), frame_state.children.size());
+    for (size_t i = 0; i < children_.size(); ++i) {
+      children_[i]->VerifyUpdatedFrameState(frame_state.children[i]);
+    }
+  }
+
+ private:
+  // Global toggle for the style of name to generate. Used to ensure that test
+  // code can consistently trigger the legacy generation path when needed.
+  static bool generate_legacy_name_;
+
+  const std::string& GetNameForCurrentMode() const {
+    return generate_legacy_name_ ? GetLegacyName() : GetUniqueName();
+  }
+
+  void CalculateLegacyName(const std::string& requested_name) {
+    // Manually skip the main frame so its legacy name is always empty: this
+    // is needed in the test as that logic lives at a different layer in
+    // UniqueNameHelper.
+    if (!IsMainFrame()) {
+      base::AutoReset<bool> enable_legacy_mode(&generate_legacy_name_, true);
+      legacy_name_ =
+          UniqueNameHelper::CalculateLegacyNameForTesting(this, requested_name);
+    }
+  }
+
+  bool CheckUniqueness(base::StringPiece name) const {
+    if (name == GetNameForCurrentMode())
+      return false;
+    for (auto* child : children_) {
+      if (!child->CheckUniqueness(name))
+        return false;
+    }
+    return true;
+  }
+
+  TestFrameAdapter* const parent_;
+  std::vector<TestFrameAdapter*> children_;
+  const int virtual_index_in_parent_;
+  UniqueNameHelper unique_name_helper_;
+  std::string legacy_name_;
+};
+
+bool TestFrameAdapter::generate_legacy_name_ = false;
+
+// Test helper that verifies that legacy unique names in versions of PageState
+// prior to 25 are correctly updated when deserialized.
+void VerifyPageStateForTargetUpdate(const TestFrameAdapter& main_frame) {
+  ExplodedPageState in_state;
+  main_frame.PopulateLegacyFrameState(&in_state.top);
+
+  // Version 24 is the last version with unlimited size unique names.
+  std::string encoded_state;
+  EncodePageStateForTesting(in_state, 24, &encoded_state);
+
+  ExplodedPageState out_state;
+  DecodePageState(encoded_state, &out_state);
+
+  main_frame.VerifyUpdatedFrameState(out_state.top);
+}
+
+TEST(UniqueNameHelper, Basic) {
+  // Main frames should always have an empty unique name.
+  TestFrameAdapter main_frame(nullptr, -1, "my main frame");
+  EXPECT_EQ("", main_frame.GetUniqueName());
+  EXPECT_EQ("", main_frame.GetLegacyName());
+
+  // A child frame with a requested name that is unique should use the requested
+  // name.
+  TestFrameAdapter frame_0(&main_frame, 0, "child frame with name");
+  EXPECT_EQ("child frame with name", frame_0.GetUniqueName());
+  EXPECT_EQ("child frame with name", frame_0.GetLegacyName());
+
+  // A child frame with no requested name should receive a generated unique
+  // name.
+  TestFrameAdapter frame_7(&main_frame, 7, "");
+  EXPECT_EQ("<!--framePath //<!--frame7-->-->", frame_7.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame7-->-->", frame_7.GetLegacyName());
+
+  // Naming collision should force a fallback to using a generated unique name.
+  TestFrameAdapter frame_2(&main_frame, 2, "child frame with name");
+  EXPECT_EQ("<!--framePath //<!--frame2-->-->", frame_2.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame2-->-->", frame_2.GetLegacyName());
+
+  // Index collision should also force a fallback to using a generated unique
+  // name.
+  TestFrameAdapter frame_2a(&main_frame, 2, "");
+  EXPECT_EQ("<!--framePath //<!--frame2-->--><!--framePosition-2/0-->",
+            frame_2a.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame2-->--><!--framePosition-2/0-->",
+            frame_2a.GetLegacyName());
+
+  // A child of a frame with a unique naming collision will incorporate the
+  // frame position marker as part of its frame path, though it will look a bit
+  // strange...
+  TestFrameAdapter frame_2a_5(&frame_2a, 5, "");
+  EXPECT_EQ(
+      "<!--framePath //<!--frame2-->--><!--framePosition-2/0/<!--frame5-->-->",
+      frame_2a_5.GetUniqueName());
+  EXPECT_EQ(
+      "<!--framePath //<!--frame2-->--><!--framePosition-2/0/<!--frame5-->-->",
+      frame_2a_5.GetLegacyName());
+
+  // Index and name collision should also force a fallback to using a generated
+  // unique name.
+  TestFrameAdapter frame_2b(&main_frame, 2, "child frame with name");
+  EXPECT_EQ("<!--framePath //<!--frame2-->--><!--framePosition-2/1-->",
+            frame_2b.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame2-->--><!--framePosition-2/1-->",
+            frame_2b.GetLegacyName());
+
+  VerifyPageStateForTargetUpdate(main_frame);
+}
+
+TEST(UniqueNameHelper, Hashing) {
+  // Main frames should always have an empty unique name.
+  TestFrameAdapter main_frame(nullptr, -1, "my main frame");
+  EXPECT_EQ("", main_frame.GetUniqueName());
+  EXPECT_EQ("", main_frame.GetLegacyName());
+
+  // A child frame with a requested name that is unique but too long should fall
+  // back to hashing.
+  const std::string too_long_name(kMaxSize + 1, 'a');
+  TestFrameAdapter frame_0(&main_frame, 0, too_long_name);
+  EXPECT_EQ(
+      "<!--"
+      "frameHash8C48280D57FB88F161ADF34D9F597D93CA32B7EDFCD23B2AFE64C3789B3F785"
+      "5-->",
+      frame_0.GetUniqueName());
+  EXPECT_EQ(too_long_name, frame_0.GetLegacyName());
+
+  // A child frame with no requested name should receive a generated unique
+  // name.
+  TestFrameAdapter frame_7(&main_frame, 7, "");
+  EXPECT_EQ("<!--framePath //<!--frame7-->-->", frame_7.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame7-->-->", frame_7.GetLegacyName());
+
+  // Verify that a requested name that's over the limit collides with the hashed
+  // version of its requested name.
+  TestFrameAdapter frame_2(&main_frame, 2, too_long_name);
+  EXPECT_EQ("<!--framePath //<!--frame2-->-->", frame_2.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame2-->-->", frame_2.GetLegacyName());
+
+  // Index collision should also force a fallback to using a generated unique
+  // name.
+  TestFrameAdapter frame_2a(&main_frame, 2, "");
+  EXPECT_EQ("<!--framePath //<!--frame2-->--><!--framePosition-2/0-->",
+            frame_2a.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame2-->--><!--framePosition-2/0-->",
+            frame_2a.GetLegacyName());
+
+  // A child of a frame with a unique naming collision will incorporate the
+  // frame position marker as part of its frame path, though it will look a bit
+  // strange...
+  TestFrameAdapter frame_2a_5(&frame_2a, 5, "");
+  EXPECT_EQ(
+      "<!--framePath //<!--frame2-->--><!--framePosition-2/0/<!--frame5-->-->",
+      frame_2a_5.GetUniqueName());
+  EXPECT_EQ(
+      "<!--framePath //<!--frame2-->--><!--framePosition-2/0/<!--frame5-->-->",
+      frame_2a_5.GetLegacyName());
+
+  // Index and name collision should also force a fallback to using a generated
+  // unique name.
+  TestFrameAdapter frame_2b(&main_frame, 2, too_long_name);
+  EXPECT_EQ("<!--framePath //<!--frame2-->--><!--framePosition-2/1-->",
+            frame_2b.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame2-->--><!--framePosition-2/1-->",
+            frame_2b.GetLegacyName());
+
+  VerifyPageStateForTargetUpdate(main_frame);
+}
+
+// Verify that basic frame path generation always includes the full path from
+// the root.
+TEST(UniqueNameHelper, BasicGeneratedFramePath) {
+  TestFrameAdapter main_frame(nullptr, -1, "my main frame");
+  EXPECT_EQ("", main_frame.GetUniqueName());
+  EXPECT_EQ("", main_frame.GetLegacyName());
+
+  TestFrameAdapter frame_2(&main_frame, 2, "");
+  EXPECT_EQ("<!--framePath //<!--frame2-->-->", frame_2.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame2-->-->", frame_2.GetLegacyName());
+
+  TestFrameAdapter frame_2_3(&frame_2, 3, "named grandchild");
+  EXPECT_EQ("named grandchild", frame_2_3.GetUniqueName());
+  EXPECT_EQ("named grandchild", frame_2_3.GetLegacyName());
+
+  // Even though the parent frame has a unique name, the frame path should
+  // include the full path from the root.
+  TestFrameAdapter frame_2_3_5(&frame_2_3, 5, "");
+  EXPECT_EQ("<!--framePath //<!--frame2-->/named grandchild/<!--frame5-->-->",
+            frame_2_3_5.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame2-->/named grandchild/<!--frame5-->-->",
+            frame_2_3_5.GetLegacyName());
+
+  VerifyPageStateForTargetUpdate(main_frame);
+}
+
+TEST(UniqueNameHelper, GeneratedFramePathHashing) {
+  TestFrameAdapter main_frame(nullptr, -1, "my main frame");
+  EXPECT_EQ("", main_frame.GetUniqueName());
+  EXPECT_EQ("", main_frame.GetLegacyName());
+
+  TestFrameAdapter frame_0(&main_frame, 0, "");
+  EXPECT_EQ("<!--framePath //<!--frame0-->-->", frame_0.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame0-->-->", frame_0.GetLegacyName());
+
+  // At the limit, so the hashing fallback should not be triggered.
+  const std::string just_fits_name(kMaxSize, 'a');
+  TestFrameAdapter frame_0_0(&frame_0, 0, just_fits_name);
+  EXPECT_EQ(just_fits_name, frame_0_0.GetUniqueName());
+  EXPECT_EQ(just_fits_name, frame_0_0.GetLegacyName());
+
+  // But anything over should trigger hashing.
+  const std::string too_long_name(kMaxSize + 1, 'a');
+  TestFrameAdapter frame_0_1(&frame_0, 1, too_long_name);
+  EXPECT_EQ(
+      "<!--"
+      "frameHash8C48280D57FB88F161ADF34D9F597D93CA32B7EDFCD23B2AFE64C3789B3F785"
+      "5-->",
+      frame_0_1.GetUniqueName());
+  EXPECT_EQ(too_long_name, frame_0_1.GetLegacyName());
+
+  // A child frame should incorporate the parent's hashed requested name into
+  // its frame path.
+  TestFrameAdapter frame_0_1_0(&frame_0_1, 0, "");
+  EXPECT_EQ(
+      "<!--framePath "
+      "//<!--frame0-->/"
+      "<!--"
+      "frameHash8C48280D57FB88F161ADF34D9F597D93CA32B7EDFCD23B2AFE64C3789B3F785"
+      "5-->/<!--frame0-->-->",
+      frame_0_1_0.GetUniqueName());
+  EXPECT_EQ(
+      "<!--framePath "
+      "//<!--frame0-->/" +
+          too_long_name + "/<!--frame0-->-->",
+      frame_0_1_0.GetLegacyName());
+
+  // Make sure that name replacement during legacy name updates don't
+  // accidentally match on substrings: the name here is intentionally chosen so
+  // that too_long_name is a substring.
+  const std::string too_long_name2(kMaxSize + 10, 'a');
+  TestFrameAdapter frame_0_2(&frame_0, 2, too_long_name2);
+  EXPECT_EQ(
+      "<!--"
+      "frameHash6B2EC79170F50EA57B886DC81A2CF78721C651A002C8365A524019A7ED5A8A4"
+      "0-->",
+      frame_0_2.GetUniqueName());
+  EXPECT_EQ(too_long_name2, frame_0_2.GetLegacyName());
+
+  // Make sure that legacy name updates correctly handle multiple replacements.
+  // An unnamed frame is used as the deepest descendant to ensure the requested
+  // names from ancestors appear in the frame path. Begin with a named
+  // grandparent:
+  const std::string too_long_name3(kMaxSize * 2, 'b');
+  TestFrameAdapter frame_0_1_1(&frame_0_1, 1, too_long_name3);
+  EXPECT_EQ(
+      "<!--"
+      "frameHash3A0B065A4255F95EF6E206B11004B8805FB631A68F468A72CE26F7592C88C27"
+      "A-->",
+      frame_0_1_1.GetUniqueName());
+  EXPECT_EQ(too_long_name3, frame_0_1_1.GetLegacyName());
+
+  // And a named parent:
+  const std::string too_long_name4(kMaxSize * 3, 'c');
+  TestFrameAdapter frame_0_1_1_0(&frame_0_1_1, 0, too_long_name4);
+  EXPECT_EQ(
+      "<!--"
+      "frameHashE00D028A784E645656638F4D461B81E779E5225CA9824C8E09664956CF4DAE3"
+      "1-->",
+      frame_0_1_1_0.GetUniqueName());
+  EXPECT_EQ(too_long_name4, frame_0_1_1_0.GetLegacyName());
+
+  // And finally an unnamed child to trigger fallback to the frame path:
+  TestFrameAdapter frame_0_1_1_0_0(&frame_0_1_1_0, 0, "");
+  EXPECT_EQ(
+      "<!--framePath "
+      "//<!--frame0-->/"
+      "<!--"
+      "frameHash8C48280D57FB88F161ADF34D9F597D93CA32B7EDFCD23B2AFE64C3789B3F785"
+      "5-->/"
+      "<!--"
+      "frameHash3A0B065A4255F95EF6E206B11004B8805FB631A68F468A72CE26F7592C88C27"
+      "A-->/"
+      "<!--"
+      "frameHashE00D028A784E645656638F4D461B81E779E5225CA9824C8E09664956CF4DAE3"
+      "1-->/<!--frame0-->-->",
+      frame_0_1_1_0_0.GetUniqueName());
+  EXPECT_EQ("<!--framePath //<!--frame0-->/" + too_long_name + "/" +
+                too_long_name3 + "/" + too_long_name4 + "/<!--frame0-->-->",
+            frame_0_1_1_0_0.GetLegacyName());
+
+  VerifyPageStateForTargetUpdate(main_frame);
+}
+
+}  // namespace
+}  // namespace content
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index 913f1a89..f2ba6ec 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -874,7 +874,7 @@
 }
 
 bool RenderFrameImpl::UniqueNameFrameAdapter::IsCandidateUnique(
-    const std::string& name) const {
+    base::StringPiece name) const {
   // This method is currently O(N), where N = number of frames in the tree.
   DCHECK(!name.empty());
 
diff --git a/content/renderer/render_frame_impl.h b/content/renderer/render_frame_impl.h
index 1f9d303..d679223 100644
--- a/content/renderer/render_frame_impl.h
+++ b/content/renderer/render_frame_impl.h
@@ -1199,7 +1199,7 @@
 
     // FrameAdapter overrides:
     bool IsMainFrame() const override;
-    bool IsCandidateUnique(const std::string& name) const override;
+    bool IsCandidateUnique(base::StringPiece name) const override;
     int GetSiblingCount() const override;
     int GetChildCount() const override;
     std::vector<base::StringPiece> CollectAncestorNames(
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index ddc1ae3..bf29c0b 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -1457,6 +1457,7 @@
     "../common/service_manager/service_manager_connection_impl_unittest.cc",
     "../common/service_worker/service_worker_utils_unittest.cc",
     "../common/throttling_url_loader_unittest.cc",
+    "../common/unique_name_helper_unittest.cc",
     "../common/webplugininfo_unittest.cc",
     "../network/network_context_unittest.cc",
     "../network/network_service_unittest.cc",
diff --git a/content/test/data/page_state/serialized_v24.dat b/content/test/data/page_state/serialized_v24.dat
new file mode 100644
index 0000000..ed726d5
--- /dev/null
+++ b/content/test/data/page_state/serialized_v24.dat
@@ -0,0 +1,17 @@
+tAMAABgAAAABAAAAEAAAAGYAaQBsAGUALgB0AHgAdAAoAAAAaAB0AHQAcAA6AC8ALwBjAGgAcgBv
+AG0AaQB1AG0ALgBvAHIAZwAvAAwAAAB0AGEAcgBnAGUAdAABAAAAKgAAANb///8kAAAAaAB0AHQA
+cAA6AC8ALwBnAG8AbwBnAGwAZQAuAGMAbwBtAC8ACAAAAGAAAAAKAA0APwAlACAAVwBlAGIASwBp
+AHQAIABzAGUAcgBpAGEAbABpAHoAZQBkACAAZgBvAHIAbQAgAHMAdABhAHQAZQAgAHYAZQByAHMA
+aQBvAG4AIAA4ACAACgANAD0AJgAQAAAAZgBvAHIAbQAgAGsAZQB5AAIAAAAxAAAABgAAAGYAbwBv
+AAAACAAAAGYAaQBsAGUAAgAAADIAAAAQAAAAZgBpAGwAZQAuAHQAeAB0ABYAAABkAGkAcwBwAGwA
+YQB5AE4AYQBtAGUAAAAIAAAAAAAAAAAAAEB7AAAAAAAAAMgBAAAAAAAAAQAAAAgAAAAAAAAAAADw
+vwgAAAAAAAAAAADwvwAAAAAAAAAAAQAAAAMAAAAAAAAAEAAAAGZpcnN0IGRhdGEgYmxvY2sBAAAA
+EAAAAGYAaQBsAGUALgB0AHgAdAAAAAAAAAAAAP//////////CAAAAAAAAAAAAAAAAAAAAA8AAABk
+YXRhIHRoZSBzZWNvbmQAFQMAAAAAAAAAAAAADgAAAGYAbwBvAC8AYgBhAHIAAAABAAAAKAAAAGgA
+dAB0AHAAOgAvAC8AYwBoAHIAbwBtAGkAdQBtAC4AbwByAGcALwD/////AQAAACoAAADW////JAAA
+AGgAdAB0AHAAOgAvAC8AZwBvAG8AZwBsAGUALgBjAG8AbQAvAAgAAABgAAAACgANAD8AJQAgAFcA
+ZQBiAEsAaQB0ACAAcwBlAHIAaQBhAGwAaQB6AGUAZAAgAGYAbwByAG0AIABzAHQAYQB0AGUAIAB2
+AGUAcgBzAGkAbwBuACAAOAAgAAoADQA9ACYAEAAAAGYAbwByAG0AIABrAGUAeQACAAAAMQAAAAYA
+AABmAG8AbwAAAAgAAABmAGkAbABlAAIAAAAyAAAAEAAAAGYAaQBsAGUALgB0AHgAdAAWAAAAZABp
+AHMAcABsAGEAeQBOAGEAbQBlAAAACAAAAAAAAAAAAABAewAAAAAAAADIAQAAAAAAAAEAAAAIAAAA
+AAAAAAAA8L8IAAAAAAAAAAAA8L8AAAAAAAAAAAAAAAD/////AAAAAA==