[Extensions] Make g_test_content_verify_job_observer thread-safe

ContentVerifyJob has ability to inject test observer, but before this CL
the observer is accessed from different threads, leading to flaky
crashes in tests. See https://crrev.com/c/2032998.

Now this observer (ContentVerifyJob::TestObserver and pointer to it from
g_test_content_verify_gob_observer in content_verify_job.cc) supports
thread-safe refcounted pointing, therefore is not destroyed too early.
Also it's wrappend into lazy initialized since we need access to it be
no-op outside of the tests.

Bug: 796395, 958794
Change-Id: I7479216f23083d4fd41dae08cb2e7acb62da0abd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2033159
Reviewed-by: Sergey Poromov <[email protected]>
Reviewed-by: Istiaque Ahmed <[email protected]>
Commit-Queue: Oleg Davydov <[email protected]>
Cr-Commit-Position: refs/heads/master@{#739387}
diff --git a/chrome/browser/extensions/extension_protocols_unittest.cc b/chrome/browser/extensions/extension_protocols_unittest.cc
index 0e5f0f31..f522ab4 100644
--- a/chrome/browser/extensions/extension_protocols_unittest.cc
+++ b/chrome/browser/extensions/extension_protocols_unittest.cc
@@ -573,7 +573,7 @@
 
   // chmod -r 1024.js.
   {
-    TestContentVerifySingleJobObserver observer(extension->id(), kRelativePath);
+    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
     base::FilePath file_path = unzipped_path.AppendASCII(kJs);
     ASSERT_TRUE(base::MakeFileUnreadable(file_path));
     EXPECT_EQ(net::ERR_ACCESS_DENIED, DoRequestOrLoad(extension, kJs).result());
@@ -637,7 +637,7 @@
   // current behavior of ContentVerifyJob.
   // TODO(lazyboy): The behavior is probably incorrect.
   {
-    TestContentVerifySingleJobObserver observer(extension->id(), kRelativePath);
+    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
     base::FilePath file_path = unzipped_path.AppendASCII(kEmptyJs);
     ASSERT_TRUE(base::MakeFileUnreadable(file_path));
     EXPECT_EQ(net::ERR_ACCESS_DENIED,
diff --git a/extensions/browser/content_verifier/test_utils.cc b/extensions/browser/content_verifier/test_utils.cc
index 10445f11..e7fa3c7 100644
--- a/extensions/browser/content_verifier/test_utils.cc
+++ b/extensions/browser/content_verifier/test_utils.cc
@@ -22,18 +22,46 @@
 TestContentVerifySingleJobObserver::TestContentVerifySingleJobObserver(
     const ExtensionId& extension_id,
     const base::FilePath& relative_path)
-    : extension_id_(extension_id), relative_path_(relative_path) {
-  ContentVerifyJob::SetObserverForTests(this);
+    : client_(
+          base::MakeRefCounted<ObserverClient>(extension_id, relative_path)) {
+  ContentVerifyJob::SetObserverForTests(client_);
 }
 
 TestContentVerifySingleJobObserver::~TestContentVerifySingleJobObserver() {
   ContentVerifyJob::SetObserverForTests(nullptr);
 }
 
-void TestContentVerifySingleJobObserver::JobFinished(
+ContentVerifyJob::FailureReason
+TestContentVerifySingleJobObserver::WaitForJobFinished() {
+  return client_->WaitForJobFinished();
+}
+
+void TestContentVerifySingleJobObserver::WaitForOnHashesReady() {
+  client_->WaitForOnHashesReady();
+}
+
+TestContentVerifySingleJobObserver::ObserverClient::ObserverClient(
+    const ExtensionId& extension_id,
+    const base::FilePath& relative_path)
+    : extension_id_(extension_id), relative_path_(relative_path) {
+  EXPECT_TRUE(
+      content::BrowserThread::GetCurrentThreadIdentifier(&creation_thread_));
+}
+
+TestContentVerifySingleJobObserver::ObserverClient::~ObserverClient() = default;
+
+void TestContentVerifySingleJobObserver::ObserverClient::JobFinished(
     const ExtensionId& extension_id,
     const base::FilePath& relative_path,
     ContentVerifyJob::FailureReason reason) {
+  if (!content::BrowserThread::CurrentlyOn(creation_thread_)) {
+    base::PostTask(
+        FROM_HERE, {creation_thread_},
+        base::BindOnce(
+            &TestContentVerifySingleJobObserver::ObserverClient::JobFinished,
+            this, extension_id, relative_path, reason));
+    return;
+  }
   if (extension_id != extension_id_ || relative_path != relative_path_)
     return;
   EXPECT_FALSE(failure_reason_.has_value());
@@ -41,10 +69,18 @@
   job_finished_run_loop_.Quit();
 }
 
-void TestContentVerifySingleJobObserver::OnHashesReady(
+void TestContentVerifySingleJobObserver::ObserverClient::OnHashesReady(
     const ExtensionId& extension_id,
     const base::FilePath& relative_path,
     bool success) {
+  if (!content::BrowserThread::CurrentlyOn(creation_thread_)) {
+    base::PostTask(
+        FROM_HERE, {creation_thread_},
+        base::BindOnce(
+            &TestContentVerifySingleJobObserver::ObserverClient::OnHashesReady,
+            this, extension_id, relative_path, success));
+    return;
+  }
   if (extension_id != extension_id_ || relative_path != relative_path_)
     return;
   EXPECT_FALSE(seen_on_hashes_ready_);
@@ -53,60 +89,57 @@
 }
 
 ContentVerifyJob::FailureReason
-TestContentVerifySingleJobObserver::WaitForJobFinished() {
+TestContentVerifySingleJobObserver::ObserverClient::WaitForJobFinished() {
   // Run() returns immediately if Quit() has already been called.
   job_finished_run_loop_.Run();
   EXPECT_TRUE(failure_reason_.has_value());
   return failure_reason_.value_or(ContentVerifyJob::FAILURE_REASON_MAX);
 }
 
-void TestContentVerifySingleJobObserver::WaitForOnHashesReady() {
+void TestContentVerifySingleJobObserver::ObserverClient::
+    WaitForOnHashesReady() {
   // Run() returns immediately if Quit() has already been called.
   on_hashes_ready_run_loop_.Run();
 }
 
 // TestContentVerifyJobObserver ------------------------------------------------
-void TestContentVerifyJobObserver::ExpectJobResult(
-    const ExtensionId& extension_id,
-    const base::FilePath& relative_path,
-    Result expected_result) {
-  expectations_.push_back(
-      ExpectedResult(extension_id, relative_path, expected_result));
-}
-
-TestContentVerifyJobObserver::TestContentVerifyJobObserver() {
-  EXPECT_TRUE(
-      content::BrowserThread::GetCurrentThreadIdentifier(&creation_thread_));
-  ContentVerifyJob::SetObserverForTests(this);
+TestContentVerifyJobObserver::TestContentVerifyJobObserver()
+    : client_(base::MakeRefCounted<ObserverClient>()) {
+  ContentVerifyJob::SetObserverForTests(client_);
 }
 
 TestContentVerifyJobObserver::~TestContentVerifyJobObserver() {
   ContentVerifyJob::SetObserverForTests(nullptr);
 }
 
-bool TestContentVerifyJobObserver::WaitForExpectedJobs() {
-  EXPECT_TRUE(content::BrowserThread::CurrentlyOn(creation_thread_));
-  if (!expectations_.empty()) {
-    base::RunLoop run_loop;
-    job_quit_closure_ = run_loop.QuitClosure();
-    run_loop.Run();
-  }
-  return expectations_.empty();
+void TestContentVerifyJobObserver::ExpectJobResult(
+    const ExtensionId& extension_id,
+    const base::FilePath& relative_path,
+    Result expected_result) {
+  client_->ExpectJobResult(extension_id, relative_path, expected_result);
 }
 
-void TestContentVerifyJobObserver::JobStarted(
-    const ExtensionId& extension_id,
-    const base::FilePath& relative_path) {}
+bool TestContentVerifyJobObserver::WaitForExpectedJobs() {
+  return client_->WaitForExpectedJobs();
+}
 
-void TestContentVerifyJobObserver::JobFinished(
+TestContentVerifyJobObserver::ObserverClient::ObserverClient() {
+  EXPECT_TRUE(
+      content::BrowserThread::GetCurrentThreadIdentifier(&creation_thread_));
+}
+
+TestContentVerifyJobObserver::ObserverClient::~ObserverClient() = default;
+
+void TestContentVerifyJobObserver::ObserverClient::JobFinished(
     const ExtensionId& extension_id,
     const base::FilePath& relative_path,
     ContentVerifyJob::FailureReason failure_reason) {
   if (!content::BrowserThread::CurrentlyOn(creation_thread_)) {
-    base::PostTask(FROM_HERE, {creation_thread_},
-                   base::BindOnce(&TestContentVerifyJobObserver::JobFinished,
-                                  base::Unretained(this), extension_id,
-                                  relative_path, failure_reason));
+    base::PostTask(
+        FROM_HERE, {creation_thread_},
+        base::BindOnce(
+            &TestContentVerifyJobObserver::ObserverClient::JobFinished, this,
+            extension_id, relative_path, failure_reason));
     return;
   }
   Result result = failure_reason == ContentVerifyJob::NONE ? Result::SUCCESS
@@ -130,6 +163,24 @@
   }
 }
 
+void TestContentVerifyJobObserver::ObserverClient::ExpectJobResult(
+    const ExtensionId& extension_id,
+    const base::FilePath& relative_path,
+    Result expected_result) {
+  expectations_.push_back(
+      ExpectedResult(extension_id, relative_path, expected_result));
+}
+
+bool TestContentVerifyJobObserver::ObserverClient::WaitForExpectedJobs() {
+  EXPECT_TRUE(content::BrowserThread::CurrentlyOn(creation_thread_));
+  if (!expectations_.empty()) {
+    base::RunLoop run_loop;
+    job_quit_closure_ = run_loop.QuitClosure();
+    run_loop.Run();
+  }
+  return expectations_.empty();
+}
+
 // MockContentVerifierDelegate ------------------------------------------------
 MockContentVerifierDelegate::MockContentVerifierDelegate() = default;
 MockContentVerifierDelegate::~MockContentVerifierDelegate() = default;
diff --git a/extensions/browser/content_verifier/test_utils.h b/extensions/browser/content_verifier/test_utils.h
index e300fa2..cac79c49e 100644
--- a/extensions/browser/content_verifier/test_utils.h
+++ b/extensions/browser/content_verifier/test_utils.h
@@ -24,22 +24,12 @@
 // Test class to observe *a particular* extension resource's ContentVerifyJob
 // lifetime.  Provides a way to wait for a job to finish and return
 // the job's result.
-class TestContentVerifySingleJobObserver : ContentVerifyJob::TestObserver {
+class TestContentVerifySingleJobObserver {
  public:
   TestContentVerifySingleJobObserver(const ExtensionId& extension_id,
                                      const base::FilePath& relative_path);
   ~TestContentVerifySingleJobObserver();
 
-  // ContentVerifyJob::TestObserver:
-  void JobStarted(const ExtensionId& extension_id,
-                  const base::FilePath& relative_path) override {}
-  void JobFinished(const ExtensionId& extension_id,
-                   const base::FilePath& relative_path,
-                   ContentVerifyJob::FailureReason reason) override;
-  void OnHashesReady(const ExtensionId& extension_id,
-                     const base::FilePath& relative_path,
-                     bool success) override;
-
   // Waits for a ContentVerifyJob to finish and returns job's status.
   ContentVerifyJob::FailureReason WaitForJobFinished() WARN_UNUSED_RESULT;
 
@@ -47,22 +37,55 @@
   void WaitForOnHashesReady();
 
  private:
-  base::RunLoop job_finished_run_loop_;
-  base::RunLoop on_hashes_ready_run_loop_;
+  class ObserverClient : public ContentVerifyJob::TestObserver {
+   public:
+    ObserverClient(const ExtensionId& extension_id,
+                   const base::FilePath& relative_path);
 
-  ExtensionId extension_id_;
-  base::FilePath relative_path_;
-  base::Optional<ContentVerifyJob::FailureReason> failure_reason_;
-  bool seen_on_hashes_ready_ = false;
+    // ContentVerifyJob::TestObserver:
+    void JobStarted(const ExtensionId& extension_id,
+                    const base::FilePath& relative_path) override {}
+    void JobFinished(const ExtensionId& extension_id,
+                     const base::FilePath& relative_path,
+                     ContentVerifyJob::FailureReason reason) override;
+    void OnHashesReady(const ExtensionId& extension_id,
+                       const base::FilePath& relative_path,
+                       bool success) override;
 
-  DISALLOW_COPY_AND_ASSIGN(TestContentVerifySingleJobObserver);
+    // Passed methods from ContentVerifySingleJobObserver:
+    ContentVerifyJob::FailureReason WaitForJobFinished() WARN_UNUSED_RESULT;
+    void WaitForOnHashesReady();
+
+   private:
+    ~ObserverClient() override;
+
+    ObserverClient(const ObserverClient&) = delete;
+    ObserverClient& operator=(const ObserverClient&) = delete;
+
+    content::BrowserThread::ID creation_thread_;
+
+    base::RunLoop job_finished_run_loop_;
+    base::RunLoop on_hashes_ready_run_loop_;
+
+    ExtensionId extension_id_;
+    base::FilePath relative_path_;
+    base::Optional<ContentVerifyJob::FailureReason> failure_reason_;
+    bool seen_on_hashes_ready_ = false;
+  };
+
+  TestContentVerifySingleJobObserver(
+      const TestContentVerifySingleJobObserver&) = delete;
+  TestContentVerifySingleJobObserver& operator=(
+      const TestContentVerifySingleJobObserver&) = delete;
+
+  scoped_refptr<ObserverClient> client_;
 };
 
 // Test class to observe expected set of ContentVerifyJobs.
-class TestContentVerifyJobObserver : public ContentVerifyJob::TestObserver {
+class TestContentVerifyJobObserver {
  public:
   TestContentVerifyJobObserver();
-  virtual ~TestContentVerifyJobObserver();
+  ~TestContentVerifyJobObserver();
 
   enum class Result { SUCCESS, FAILURE };
 
@@ -75,34 +98,56 @@
   // finish, or false if there was an error or timeout.
   bool WaitForExpectedJobs();
 
-  // ContentVerifyJob::TestObserver interface
-  void JobStarted(const ExtensionId& extension_id,
-                  const base::FilePath& relative_path) override;
-  void JobFinished(const ExtensionId& extension_id,
-                   const base::FilePath& relative_path,
-                   ContentVerifyJob::FailureReason failure_reason) override;
-  void OnHashesReady(const ExtensionId& extension_id,
-                     const base::FilePath& relative_path,
-                     bool success) override {}
-
  private:
-  struct ExpectedResult {
+  class ObserverClient : public ContentVerifyJob::TestObserver {
    public:
-    ExtensionId extension_id;
-    base::FilePath path;
-    Result result;
+    ObserverClient();
 
-    ExpectedResult(const ExtensionId& extension_id,
-                   const base::FilePath& path,
-                   Result result)
-        : extension_id(extension_id), path(path), result(result) {}
+    // ContentVerifyJob::TestObserver:
+    void JobStarted(const ExtensionId& extension_id,
+                    const base::FilePath& relative_path) override {}
+    void JobFinished(const ExtensionId& extension_id,
+                     const base::FilePath& relative_path,
+                     ContentVerifyJob::FailureReason failure_reason) override;
+    void OnHashesReady(const ExtensionId& extension_id,
+                       const base::FilePath& relative_path,
+                       bool success) override {}
+
+    // Passed methods from TestContentVerifyJobObserver:
+    void ExpectJobResult(const ExtensionId& extension_id,
+                         const base::FilePath& relative_path,
+                         Result expected_result);
+    bool WaitForExpectedJobs();
+
+   private:
+    struct ExpectedResult {
+     public:
+      ExtensionId extension_id;
+      base::FilePath path;
+      Result result;
+
+      ExpectedResult(const ExtensionId& extension_id,
+                     const base::FilePath& path,
+                     Result result)
+          : extension_id(extension_id), path(path), result(result) {}
+    };
+
+    ~ObserverClient() override;
+
+    ObserverClient(const ObserverClient&) = delete;
+    ObserverClient& operator=(const ObserverClient&) = delete;
+
+    std::list<ExpectedResult> expectations_;
+    content::BrowserThread::ID creation_thread_;
+    // Accessed on |creation_thread_|.
+    base::OnceClosure job_quit_closure_;
   };
-  std::list<ExpectedResult> expectations_;
-  content::BrowserThread::ID creation_thread_;
-  // Accessed on |creation_thread_|.
-  base::OnceClosure job_quit_closure_;
 
-  DISALLOW_COPY_AND_ASSIGN(TestContentVerifyJobObserver);
+  TestContentVerifyJobObserver(const TestContentVerifyJobObserver&) = delete;
+  TestContentVerifyJobObserver& operator=(const TestContentVerifyJobObserver&) =
+      delete;
+
+  scoped_refptr<ObserverClient> client_;
 };
 
 // An extensions/ implementation of ContentVerifierDelegate for using in tests.
diff --git a/extensions/browser/content_verify_job.cc b/extensions/browser/content_verify_job.cc
index 036f14e3..6e38444 100644
--- a/extensions/browser/content_verify_job.cc
+++ b/extensions/browser/content_verify_job.cc
@@ -7,6 +7,7 @@
 #include <algorithm>
 
 #include "base/bind.h"
+#include "base/lazy_instance.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/stl_util.h"
 #include "base/task/post_task.h"
@@ -23,7 +24,15 @@
 namespace {
 
 bool g_ignore_verification_for_tests = false;
-ContentVerifyJob::TestObserver* g_content_verify_job_test_observer = NULL;
+
+base::LazyInstance<scoped_refptr<ContentVerifyJob::TestObserver>>::Leaky
+    g_content_verify_job_test_observer = LAZY_INSTANCE_INITIALIZER;
+
+scoped_refptr<ContentVerifyJob::TestObserver> GetTestObserver() {
+  if (!g_content_verify_job_test_observer.IsCreated())
+    return nullptr;
+  return g_content_verify_job_test_observer.Get();
+}
 
 class ScopedElapsedTimer {
  public:
@@ -86,9 +95,9 @@
     scoped_refptr<const ContentHash> content_hash) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
   base::AutoLock auto_lock(lock_);
-  if (g_content_verify_job_test_observer)
-    g_content_verify_job_test_observer->JobStarted(extension_id_,
-                                                   relative_path_);
+  scoped_refptr<TestObserver> test_observer = GetTestObserver();
+  if (test_observer)
+    test_observer->JobStarted(extension_id_, relative_path_);
   // Build |hash_reader_|.
   base::PostTaskAndReplyWithResult(
       FROM_HERE,
@@ -119,10 +128,9 @@
 
   const bool can_proceed = has_ignorable_read_error_ || FinishBlock();
   if (can_proceed) {
-    if (g_content_verify_job_test_observer) {
-      g_content_verify_job_test_observer->JobFinished(extension_id_,
-                                                      relative_path_, NONE);
-    }
+    scoped_refptr<TestObserver> test_observer = GetTestObserver();
+    if (test_observer)
+      test_observer->JobFinished(extension_id_, relative_path_, NONE);
   } else {
     DispatchFailureCallback(HASH_MISMATCH);
   }
@@ -214,10 +222,9 @@
 
   if (g_ignore_verification_for_tests)
     return;
-  if (g_content_verify_job_test_observer) {
-    g_content_verify_job_test_observer->OnHashesReady(extension_id_,
-                                                      relative_path_, success);
-  }
+  scoped_refptr<TestObserver> test_observer = GetTestObserver();
+  if (test_observer)
+    test_observer->OnHashesReady(extension_id_, relative_path_, success);
   if (!success) {
     if (!hash_reader_->has_content_hashes()) {
       DispatchFailureCallback(MISSING_ALL_HASHES);
@@ -226,10 +233,9 @@
 
     if (hash_reader_->file_missing_from_verified_contents()) {
       // Ignore verification of non-existent resources.
-      if (g_content_verify_job_test_observer) {
-        g_content_verify_job_test_observer->JobFinished(extension_id_,
-                                                        relative_path_, NONE);
-      }
+      scoped_refptr<TestObserver> test_observer = GetTestObserver();
+      if (test_observer)
+        test_observer->JobFinished(extension_id_, relative_path_, NONE);
       return;
     }
     DispatchFailureCallback(NO_HASHES_FOR_FILE);
@@ -250,9 +256,10 @@
     ScopedElapsedTimer timer(&time_spent_);
     if (!has_ignorable_read_error_ && !FinishBlock()) {
       DispatchFailureCallback(HASH_MISMATCH);
-    } else if (g_content_verify_job_test_observer) {
-      g_content_verify_job_test_observer->JobFinished(extension_id_,
-                                                      relative_path_, NONE);
+    } else {
+      scoped_refptr<TestObserver> test_observer = GetTestObserver();
+      if (test_observer)
+        test_observer->JobFinished(extension_id_, relative_path_, NONE);
     }
   }
 }
@@ -264,11 +271,13 @@
 }
 
 // static
-void ContentVerifyJob::SetObserverForTests(TestObserver* observer) {
-  DCHECK(observer == nullptr || g_content_verify_job_test_observer == nullptr)
+void ContentVerifyJob::SetObserverForTests(
+    scoped_refptr<TestObserver> observer) {
+  DCHECK(observer == nullptr ||
+         g_content_verify_job_test_observer.Get() == nullptr)
       << "SetObserverForTests does not support interleaving. Observers should "
       << "be set and then cleared one at a time.";
-  g_content_verify_job_test_observer = observer;
+  g_content_verify_job_test_observer.Get() = std::move(observer);
 }
 
 void ContentVerifyJob::DispatchFailureCallback(FailureReason reason) {
@@ -279,10 +288,9 @@
             << relative_path_.MaybeAsASCII() << " reason:" << reason;
     std::move(failure_callback_).Run(reason);
   }
-  if (g_content_verify_job_test_observer) {
-    g_content_verify_job_test_observer->JobFinished(extension_id_,
-                                                    relative_path_, reason);
-  }
+  scoped_refptr<TestObserver> test_observer = GetTestObserver();
+  if (test_observer)
+    test_observer->JobFinished(extension_id_, relative_path_, reason);
 }
 
 }  // namespace extensions
diff --git a/extensions/browser/content_verify_job.h b/extensions/browser/content_verify_job.h
index 8c6d439b..782e783 100644
--- a/extensions/browser/content_verify_job.h
+++ b/extensions/browser/content_verify_job.h
@@ -80,7 +80,7 @@
   // is not so appropriate.
   void Done();
 
-  class TestObserver {
+  class TestObserver : public base::RefCountedThreadSafe<TestObserver> {
    public:
     virtual void JobStarted(const ExtensionId& extension_id,
                             const base::FilePath& relative_path) = 0;
@@ -92,12 +92,16 @@
     virtual void OnHashesReady(const ExtensionId& extension_id,
                                const base::FilePath& relative_path,
                                bool success) = 0;
+
+   protected:
+    virtual ~TestObserver() = default;
+    friend class base::RefCountedThreadSafe<TestObserver>;
   };
 
   static void SetIgnoreVerificationForTests(bool value);
 
   // Note: having interleaved observer is not supported.
-  static void SetObserverForTests(TestObserver* observer);
+  static void SetObserverForTests(scoped_refptr<TestObserver> observer);
 
  private:
   virtual ~ContentVerifyJob();