// Copyright 2014 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 "extensions/browser/content_verify_job.h" #include #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" #include "base/task/thread_pool.h" #include "base/timer/elapsed_timer.h" #include "content/public/browser/browser_thread.h" #include "crypto/secure_hash.h" #include "crypto/sha2.h" #include "extensions/browser/content_hash_reader.h" #include "extensions/browser/content_verifier.h" #include "extensions/browser/content_verifier/content_hash.h" namespace extensions { namespace { bool g_ignore_verification_for_tests = false; base::LazyInstance>::Leaky g_content_verify_job_test_observer = LAZY_INSTANCE_INITIALIZER; scoped_refptr GetTestObserver() { if (!g_content_verify_job_test_observer.IsCreated()) return nullptr; return g_content_verify_job_test_observer.Get(); } class ScopedElapsedTimer { public: explicit ScopedElapsedTimer(base::TimeDelta* total) : total_(total) { DCHECK(total_); } ~ScopedElapsedTimer() { *total_ += timer.Elapsed(); } private: // Some total amount of time we should add our elapsed time to at // destruction. base::TimeDelta* total_; // A timer for how long this object has been alive. base::ElapsedTimer timer; }; bool IsIgnorableReadError(MojoResult read_result) { // Extension reload, for example, can cause benign MOJO_RESULT_ABORTED error. // Do not incorrectly fail content verification in that case. // See https://crbug.com/977805 for details. return read_result == MOJO_RESULT_ABORTED; } } // namespace ContentVerifyJob::ContentVerifyJob(const ExtensionId& extension_id, const base::Version& extension_version, const base::FilePath& extension_root, const base::FilePath& relative_path, FailureCallback failure_callback) : done_reading_(false), hashes_ready_(false), total_bytes_read_(0), current_block_(0), current_hash_byte_count_(0), extension_id_(extension_id), extension_version_(extension_version), extension_root_(extension_root), relative_path_(relative_path), failure_callback_(std::move(failure_callback)), failed_(false) {} ContentVerifyJob::~ContentVerifyJob() { UMA_HISTOGRAM_COUNTS_1M("ExtensionContentVerifyJob.TimeSpentUS", time_spent_.InMicroseconds()); } void ContentVerifyJob::Start(ContentVerifier* verifier) { DCHECK_CURRENTLY_ON(content::BrowserThread::IO); base::AutoLock auto_lock(lock_); verifier->GetContentHash( extension_id_, extension_root_, extension_version_, true /* force_missing_computed_hashes_creation */, base::BindOnce(&ContentVerifyJob::DidGetContentHashOnIO, this)); } void ContentVerifyJob::DidGetContentHashOnIO( scoped_refptr content_hash) { DCHECK_CURRENTLY_ON(content::BrowserThread::IO); base::AutoLock auto_lock(lock_); scoped_refptr test_observer = GetTestObserver(); if (test_observer) test_observer->JobStarted(extension_id_, relative_path_); // Build |hash_reader_|. base::ThreadPool::PostTaskAndReplyWithResult( FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE}, base::BindOnce(&ContentHashReader::Create, relative_path_, content_hash), base::BindOnce(&ContentVerifyJob::OnHashesReady, this)); } void ContentVerifyJob::Read(const char* data, int count, MojoResult read_result) { base::AutoLock auto_lock(lock_); DCHECK(!done_reading_); ReadImpl(data, count, read_result); } void ContentVerifyJob::Done() { base::AutoLock auto_lock(lock_); ScopedElapsedTimer timer(&time_spent_); if (failed_) return; if (g_ignore_verification_for_tests) return; DCHECK(!done_reading_); done_reading_ = true; if (!hashes_ready_) return; // Wait for OnHashesReady. const bool can_proceed = has_ignorable_read_error_ || FinishBlock(); if (can_proceed) { scoped_refptr test_observer = GetTestObserver(); if (test_observer) test_observer->JobFinished(extension_id_, relative_path_, NONE); } else { DispatchFailureCallback(HASH_MISMATCH); } } void ContentVerifyJob::ReadImpl(const char* data, int count, MojoResult read_result) { ScopedElapsedTimer timer(&time_spent_); if (failed_) return; if (g_ignore_verification_for_tests) return; if (IsIgnorableReadError(read_result)) has_ignorable_read_error_ = true; if (has_ignorable_read_error_) return; if (!hashes_ready_) { queue_.append(data, count); return; } DCHECK_GE(count, 0); int bytes_added = 0; while (bytes_added < count) { if (current_block_ >= hash_reader_->block_count()) return DispatchFailureCallback(HASH_MISMATCH); if (!current_hash_) { current_hash_byte_count_ = 0; current_hash_ = crypto::SecureHash::Create(crypto::SecureHash::SHA256); } // Compute how many bytes we should hash, and add them to the current hash. int bytes_to_hash = std::min(hash_reader_->block_size() - current_hash_byte_count_, count - bytes_added); DCHECK_GT(bytes_to_hash, 0); current_hash_->Update(data + bytes_added, bytes_to_hash); bytes_added += bytes_to_hash; current_hash_byte_count_ += bytes_to_hash; total_bytes_read_ += bytes_to_hash; // If we finished reading a block worth of data, finish computing the hash // for it and make sure the expected hash matches. if (current_hash_byte_count_ == hash_reader_->block_size() && !FinishBlock()) { DispatchFailureCallback(HASH_MISMATCH); return; } } } bool ContentVerifyJob::FinishBlock() { DCHECK(!failed_); if (current_hash_byte_count_ == 0) { if (!done_reading_ || // If we have checked all blocks already, then nothing else to do here. current_block_ == hash_reader_->block_count()) { return true; } } if (!current_hash_) { // This happens when we fail to read the resource. Compute empty content's // hash in this case. current_hash_ = crypto::SecureHash::Create(crypto::SecureHash::SHA256); } std::string final(crypto::kSHA256Length, 0); current_hash_->Finish(base::data(final), final.size()); current_hash_.reset(); current_hash_byte_count_ = 0; int block = current_block_++; const std::string* expected_hash = NULL; if (!hash_reader_->GetHashForBlock(block, &expected_hash) || *expected_hash != final) { return false; } return true; } void ContentVerifyJob::OnHashesReady( std::unique_ptr hash_reader) { base::AutoLock auto_lock(lock_); hash_reader_ = std::move(hash_reader); if (g_ignore_verification_for_tests) return; scoped_refptr test_observer = GetTestObserver(); if (test_observer) test_observer->OnHashesReady(extension_id_, relative_path_, *hash_reader_); switch (hash_reader_->status()) { case ContentHashReader::InitStatus::HASHES_MISSING: { DispatchFailureCallback(MISSING_ALL_HASHES); return; } case ContentHashReader::InitStatus::HASHES_DAMAGED: { DispatchFailureCallback(CORRUPTED_HASHES); return; } case ContentHashReader::InitStatus::NO_HASHES_FOR_NON_EXISTING_RESOURCE: { // Ignore verification of non-existent resources. scoped_refptr test_observer = GetTestObserver(); if (test_observer) test_observer->JobFinished(extension_id_, relative_path_, NONE); return; } case ContentHashReader::InitStatus::NO_HASHES_FOR_RESOURCE: { DispatchFailureCallback(NO_HASHES_FOR_FILE); return; } case ContentHashReader::InitStatus::SUCCESS: { // Just proceed with hashes in case of success. } } DCHECK_EQ(ContentHashReader::InitStatus::SUCCESS, hash_reader_->status()); DCHECK(!failed_); hashes_ready_ = true; if (!queue_.empty()) { std::string tmp; queue_.swap(tmp); ReadImpl(base::data(tmp), tmp.size(), MOJO_RESULT_OK); if (failed_) return; } if (done_reading_) { ScopedElapsedTimer timer(&time_spent_); if (!has_ignorable_read_error_ && !FinishBlock()) { DispatchFailureCallback(HASH_MISMATCH); } else { scoped_refptr test_observer = GetTestObserver(); if (test_observer) test_observer->JobFinished(extension_id_, relative_path_, NONE); } } } // static void ContentVerifyJob::SetIgnoreVerificationForTests(bool value) { DCHECK_NE(g_ignore_verification_for_tests, value); g_ignore_verification_for_tests = value; } // static void ContentVerifyJob::SetObserverForTests( scoped_refptr 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.Get() = std::move(observer); } void ContentVerifyJob::DispatchFailureCallback(FailureReason reason) { DCHECK(!failed_); failed_ = true; if (!failure_callback_.is_null()) { VLOG(1) << "job failed for " << extension_id_ << " " << relative_path_.MaybeAsASCII() << " reason:" << reason; std::move(failure_callback_).Run(reason); } scoped_refptr test_observer = GetTestObserver(); if (test_observer) test_observer->JobFinished(extension_id_, relative_path_, reason); } } // namespace extensions