From fd3df77805388c12b7a20149c4fc4a8c061df26d Mon Sep 17 00:00:00 2001 From: "asargent@chromium.org" Date: Thu, 8 May 2014 23:54:27 +0000 Subject: Beginning of support for extension content verification The basic idea is that the webstore will provide signed expected hashes of file content that can be checked during runtime in the browser to detect corruption due to disk errors or malware. This CL has a lot of the high-level pieces, with several of the details left out for subsequent CLs to make this one more easily digestible. The design is that there is a ContentVerifier for each BrowserContext which can be used anywhere we read from files inside an extension. It vends out ContentVerifyJob's, which need to be informed of the bytes read from each file, and will know how to check those against a set of expected block hashes. If the job detects contents that don't match what was expected, it will notify the verifier. BUG=369895 R=rvargas@chromium.org, yoz@chromium.org Review URL: https://codereview.chromium.org/266963003 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@269108 0039d316-1c4b-4281-b951-d872f2087c98 --- apps/shell/browser/shell_extension_system.cc | 4 + apps/shell/browser/shell_extension_system.h | 1 + .../extensions/content_verifier_browsertest.cc | 152 +++++++++++++++++++++ chrome/browser/extensions/extension_service.cc | 4 + chrome/browser/extensions/extension_service.h | 7 +- chrome/browser/extensions/extension_system_impl.cc | 27 ++++ chrome/browser/extensions/extension_system_impl.h | 6 + chrome/browser/extensions/test_extension_system.cc | 4 + chrome/browser/extensions/test_extension_system.h | 1 + chrome/browser/extensions/user_script_master.cc | 47 ++++++- chrome/browser/extensions/user_script_master.h | 8 +- chrome/chrome_tests.gypi | 1 + .../test/data/extensions/content_verifier/v1.crx | Bin 0 -> 909 bytes .../extensions/content_verifier/v1/manifest.json | 6 + .../data/extensions/content_verifier/v1/page.html | 1 + extensions/browser/content_verifier.cc | 125 +++++++++++++++++ extensions/browser/content_verifier.h | 103 ++++++++++++++ extensions/browser/content_verifier_filter.h | 23 ++++ extensions/browser/content_verify_job.cc | 69 ++++++++++ extensions/browser/content_verify_job.h | 97 +++++++++++++ extensions/browser/extension_protocols.cc | 61 +++++---- extensions/browser/extension_protocols.h | 2 +- extensions/browser/extension_system.h | 4 + extensions/browser/info_map.cc | 5 + extensions/browser/info_map.h | 6 + extensions/common/switches.cc | 10 ++ extensions/common/switches.h | 4 + extensions/extensions.gyp | 5 + net/url_request/url_request_file_job.cc | 16 ++- net/url_request/url_request_file_job.h | 2 + 30 files changed, 760 insertions(+), 41 deletions(-) create mode 100644 chrome/browser/extensions/content_verifier_browsertest.cc create mode 100644 chrome/test/data/extensions/content_verifier/v1.crx create mode 100644 chrome/test/data/extensions/content_verifier/v1/manifest.json create mode 100644 chrome/test/data/extensions/content_verifier/v1/page.html create mode 100644 extensions/browser/content_verifier.cc create mode 100644 extensions/browser/content_verifier.h create mode 100644 extensions/browser/content_verifier_filter.h create mode 100644 extensions/browser/content_verify_job.cc create mode 100644 extensions/browser/content_verify_job.h diff --git a/apps/shell/browser/shell_extension_system.cc b/apps/shell/browser/shell_extension_system.cc index f6a3b85..fffba35 100644 --- a/apps/shell/browser/shell_extension_system.cc +++ b/apps/shell/browser/shell_extension_system.cc @@ -170,4 +170,8 @@ const OneShotEvent& ShellExtensionSystem::ready() const { return ready_; } +ContentVerifier* ShellExtensionSystem::content_verifier() { + return NULL; +} + } // namespace extensions diff --git a/apps/shell/browser/shell_extension_system.h b/apps/shell/browser/shell_extension_system.h index 7f39211..a287d2d 100644 --- a/apps/shell/browser/shell_extension_system.h +++ b/apps/shell/browser/shell_extension_system.h @@ -67,6 +67,7 @@ class ShellExtensionSystem : public ExtensionSystem { const std::string& extension_id, const UnloadedExtensionInfo::Reason reason) OVERRIDE; virtual const OneShotEvent& ready() const OVERRIDE; + virtual ContentVerifier* content_verifier() OVERRIDE; private: content::BrowserContext* browser_context_; // Not owned. diff --git a/chrome/browser/extensions/content_verifier_browsertest.cc b/chrome/browser/extensions/content_verifier_browsertest.cc new file mode 100644 index 0000000..f657c34 --- /dev/null +++ b/chrome/browser/extensions/content_verifier_browsertest.cc @@ -0,0 +1,152 @@ +// 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 "base/scoped_observer.h" +#include "chrome/browser/extensions/extension_browsertest.h" +#include "content/public/test/test_utils.h" +#include "extensions/browser/content_verify_job.h" +#include "extensions/browser/extension_prefs.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/browser/extension_registry_observer.h" +#include "extensions/common/switches.h" + +namespace extensions { + +namespace { + +// Helper for observing extension unloads. +class UnloadObserver : public ExtensionRegistryObserver { + public: + explicit UnloadObserver(ExtensionRegistry* registry) : observer_(this) { + observer_.Add(registry); + } + virtual ~UnloadObserver() {} + + void WaitForUnload(const ExtensionId& id) { + if (ContainsKey(observed_, id)) + return; + + ASSERT_TRUE(loop_runner_.get() == NULL); + awaited_id_ = id; + loop_runner_ = new content::MessageLoopRunner(); + loop_runner_->Run(); + } + + virtual void OnExtensionUnloaded( + content::BrowserContext* browser_context, + const Extension* extension, + UnloadedExtensionInfo::Reason reason) OVERRIDE { + observed_.insert(extension->id()); + if (awaited_id_ == extension->id()) + loop_runner_->Quit(); + } + + private: + ExtensionId awaited_id_; + std::set observed_; + scoped_refptr loop_runner_; + ScopedObserver observer_; +}; + +// Helper for forcing ContentVerifyJob's to return an error. +class JobDelegate : public ContentVerifyJob::TestDelegate { + public: + JobDelegate() : fail_next_read_(false), fail_next_done_(false) {} + + virtual ~JobDelegate() {} + + void set_id(const ExtensionId& id) { id_ = id; } + void fail_next_read() { fail_next_read_ = true; } + void fail_next_done() { fail_next_done_ = true; } + + virtual ContentVerifyJob::FailureReason BytesRead(const ExtensionId& id, + int count, + const char* data) OVERRIDE { + if (id == id_ && fail_next_read_) { + fail_next_read_ = false; + return ContentVerifyJob::HASH_MISMATCH; + } + return ContentVerifyJob::NONE; + } + + virtual ContentVerifyJob::FailureReason DoneReading( + const ExtensionId& id) OVERRIDE { + if (id == id_ && fail_next_done_) { + fail_next_done_ = false; + return ContentVerifyJob::HASH_MISMATCH; + } + return ContentVerifyJob::NONE; + } + + private: + DISALLOW_COPY_AND_ASSIGN(JobDelegate); + + ExtensionId id_; + bool fail_next_read_; + bool fail_next_done_; +}; + +} // namespace + +class ContentVerifierTest : public ExtensionBrowserTest { + public: + ContentVerifierTest() {} + virtual ~ContentVerifierTest() {} + + virtual void SetUpCommandLine(base::CommandLine* command_line) OVERRIDE { + ExtensionBrowserTest::SetUpCommandLine(command_line); + command_line->AppendSwitchASCII( + switches::kExtensionContentVerification, + switches::kExtensionContentVerificationEnforce); + } + + // Setup our unload observer and JobDelegate, and install a test extension. + virtual void SetUpOnMainThread() OVERRIDE { + ExtensionBrowserTest::SetUpOnMainThread(); + unload_observer_.reset( + new UnloadObserver(ExtensionRegistry::Get(profile()))); + const Extension* extension = InstallExtensionFromWebstore( + test_data_dir_.AppendASCII("content_verifier/v1.crx"), 1); + ASSERT_TRUE(extension); + id_ = extension->id(); + page_url_ = extension->GetResourceURL("page.html"); + delegate_.set_id(id_); + ContentVerifyJob::SetDelegateForTests(&delegate_); + } + + virtual void TearDownOnMainThread() OVERRIDE { + ContentVerifyJob::SetDelegateForTests(NULL); + ExtensionBrowserTest::TearDownOnMainThread(); + } + + virtual void OpenPageAndWaitForUnload() { + AddTabAtIndex(1, page_url_, content::PAGE_TRANSITION_LINK); + unload_observer_->WaitForUnload(id_); + ExtensionPrefs* prefs = ExtensionPrefs::Get(profile()); + int reasons = prefs->GetDisableReasons(id_); + EXPECT_TRUE(reasons & Extension::DISABLE_CORRUPTED); + + // This needs to happen before the ExtensionRegistry gets deleted, which + // happens before TearDownOnMainThread is called. + unload_observer_.reset(); + } + + protected: + JobDelegate delegate_; + scoped_ptr unload_observer_; + ExtensionId id_; + GURL page_url_; +}; + +IN_PROC_BROWSER_TEST_F(ContentVerifierTest, FailOnRead) { + delegate_.fail_next_read(); + OpenPageAndWaitForUnload(); +} + +IN_PROC_BROWSER_TEST_F(ContentVerifierTest, FailOnDone) { + delegate_.fail_next_done(); + OpenPageAndWaitForUnload(); +} + +} // namespace extensions diff --git a/chrome/browser/extensions/extension_service.cc b/chrome/browser/extensions/extension_service.cc index 037abdb..fd12b351 100644 --- a/chrome/browser/extensions/extension_service.cc +++ b/chrome/browser/extensions/extension_service.cc @@ -2395,6 +2395,10 @@ void ExtensionService::UpdateGreylistedExtensions( } } +void ExtensionService::ContentVerifyFailed(const std::string& extension_id) { + DisableExtension(extension_id, Extension::DISABLE_CORRUPTED); +} + void ExtensionService::AddUpdateObserver(extensions::UpdateObserver* observer) { update_observers_.AddObserver(observer); } diff --git a/chrome/browser/extensions/extension_service.h b/chrome/browser/extensions/extension_service.h index 03f18a1..ca80400 100644 --- a/chrome/browser/extensions/extension_service.h +++ b/chrome/browser/extensions/extension_service.h @@ -24,6 +24,7 @@ #include "content/public/browser/devtools_agent_host.h" #include "content/public/browser/notification_observer.h" #include "content/public/browser/notification_registrar.h" +#include "extensions/browser/content_verifier.h" #include "extensions/browser/extension_function_histogram_value.h" #include "extensions/browser/extension_prefs.h" #include "extensions/browser/external_provider_interface.h" @@ -126,7 +127,8 @@ class ExtensionService : public ExtensionServiceInterface, public extensions::ExternalProviderInterface::VisitorInterface, public content::NotificationObserver, - public extensions::Blacklist::Observer { + public extensions::Blacklist::Observer, + public extensions::ContentVerifierObserver { public: // Attempts to uninstall an extension from a given ExtensionService. Returns // true iff the target extension exists. @@ -471,6 +473,9 @@ class ExtensionService external_updates_finished_callback_ = callback; } + // ContentVerifierObserver implementation. + virtual void ContentVerifyFailed(const std::string& extension_id) OVERRIDE; + // Adds/Removes update observers. void AddUpdateObserver(extensions::UpdateObserver* observer); void RemoveUpdateObserver(extensions::UpdateObserver* observer); diff --git a/chrome/browser/extensions/extension_system_impl.cc b/chrome/browser/extensions/extension_system_impl.cc index 2e4d293..924890a 100644 --- a/chrome/browser/extensions/extension_system_impl.cc +++ b/chrome/browser/extensions/extension_system_impl.cc @@ -32,8 +32,10 @@ #include "chrome/common/chrome_switches.h" #include "chrome/common/chrome_version_info.h" #include "chrome/common/extensions/features/feature_channel.h" +#include "chrome/common/extensions/manifest_url_handler.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/url_data_source.h" +#include "extensions/browser/content_verifier.h" #include "extensions/browser/event_router.h" #include "extensions/browser/extension_pref_store.h" #include "extensions/browser/extension_pref_value_map.h" @@ -137,6 +139,12 @@ void ExtensionSystemImpl::Shared::RegisterManagementPolicyProviders() { #endif // defined(ENABLE_EXTENSIONS) } +static bool ShouldVerifyExtensionContent(const Extension* extension) { + return ((extension->is_extension() || extension->is_legacy_packaged_app()) && + ManifestURL::UpdatesFromGallery(extension) && + Manifest::IsAutoUpdateableLocation(extension->location())); +} + void ExtensionSystemImpl::Shared::Init(bool extensions_enabled) { const CommandLine* command_line = CommandLine::ForCurrentProcess(); @@ -171,6 +179,11 @@ void ExtensionSystemImpl::Shared::Init(bool extensions_enabled) { install_verifier_.reset( new InstallVerifier(ExtensionPrefs::Get(profile_), profile_)); install_verifier_->Init(); + ContentVerifierFilter filter = base::Bind(&ShouldVerifyExtensionContent); + content_verifier_ = new ContentVerifier(profile_, filter); + content_verifier_->AddObserver(extension_service_.get()); + content_verifier_->Start(); + info_map()->SetContentVerifier(content_verifier_.get()); management_policy_.reset(new ManagementPolicy); RegisterManagementPolicyProviders(); @@ -243,6 +256,12 @@ void ExtensionSystemImpl::Shared::Shutdown() { extension_warning_service_->RemoveObserver( extension_warning_badge_service_.get()); } + if (content_verifier_) { + if (extension_service_) + content_verifier_->RemoveObserver(extension_service_.get()); + content_verifier_->Shutdown(); + } + if (extension_service_) extension_service_->Shutdown(); } @@ -306,6 +325,10 @@ QuotaService* ExtensionSystemImpl::Shared::quota_service() { return quota_service_.get(); } +ContentVerifier* ExtensionSystemImpl::Shared::content_verifier() { + return content_verifier_.get(); +} + // // ExtensionSystemImpl // @@ -403,6 +426,10 @@ QuotaService* ExtensionSystemImpl::quota_service() { return shared_->quota_service(); } +ContentVerifier* ExtensionSystemImpl::content_verifier() { + return shared_->content_verifier(); +} + void ExtensionSystemImpl::RegisterExtensionWithRequestContexts( const Extension* extension) { base::Time install_time; diff --git a/chrome/browser/extensions/extension_system_impl.h b/chrome/browser/extensions/extension_system_impl.h index 90c8c82..8d5b1d6 100644 --- a/chrome/browser/extensions/extension_system_impl.h +++ b/chrome/browser/extensions/extension_system_impl.h @@ -12,6 +12,7 @@ class Profile; namespace extensions { +class ContentVerifier; class ExtensionSystemSharedFactory; class ExtensionWarningBadgeService; class NavigationObserver; @@ -57,6 +58,7 @@ class ExtensionSystemImpl : public ExtensionSystem { const UnloadedExtensionInfo::Reason reason) OVERRIDE; virtual const OneShotEvent& ready() const OVERRIDE; + virtual ContentVerifier* content_verifier() OVERRIDE; // shared private: friend class ExtensionSystemSharedFactory; @@ -92,6 +94,7 @@ class ExtensionSystemImpl : public ExtensionSystem { InstallVerifier* install_verifier(); QuotaService* quota_service(); const OneShotEvent& ready() const { return ready_; } + ContentVerifier* content_verifier(); private: Profile* profile_; @@ -122,6 +125,9 @@ class ExtensionSystemImpl : public ExtensionSystem { scoped_ptr install_verifier_; scoped_ptr quota_service_; + // For verifying the contents of extensions read from disk. + scoped_refptr content_verifier_; + #if defined(OS_CHROMEOS) scoped_ptr device_local_account_management_policy_provider_; diff --git a/chrome/browser/extensions/test_extension_system.cc b/chrome/browser/extensions/test_extension_system.cc index 50948d5..1a062ac 100644 --- a/chrome/browser/extensions/test_extension_system.cc +++ b/chrome/browser/extensions/test_extension_system.cc @@ -183,6 +183,10 @@ const OneShotEvent& TestExtensionSystem::ready() const { return ready_; } +ContentVerifier* TestExtensionSystem::content_verifier() { + return NULL; +} + // static KeyedService* TestExtensionSystem::Build(content::BrowserContext* profile) { return new TestExtensionSystem(static_cast(profile)); diff --git a/chrome/browser/extensions/test_extension_system.h b/chrome/browser/extensions/test_extension_system.h index ad5375a..82d7cb3 100644 --- a/chrome/browser/extensions/test_extension_system.h +++ b/chrome/browser/extensions/test_extension_system.h @@ -77,6 +77,7 @@ class TestExtensionSystem : public ExtensionSystem { virtual InstallVerifier* install_verifier() OVERRIDE; virtual QuotaService* quota_service() OVERRIDE; virtual const OneShotEvent& ready() const OVERRIDE; + virtual ContentVerifier* content_verifier() OVERRIDE; void SetReady() { LOG(INFO) << "SetReady()"; diff --git a/chrome/browser/extensions/user_script_master.cc b/chrome/browser/extensions/user_script_master.cc index 8a56f45..ccf7f19 100644 --- a/chrome/browser/extensions/user_script_master.cc +++ b/chrome/browser/extensions/user_script_master.cc @@ -19,7 +19,9 @@ #include "chrome/common/extensions/manifest_handlers/content_scripts_handler.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/render_process_host.h" +#include "extensions/browser/content_verifier.h" #include "extensions/browser/extension_registry.h" +#include "extensions/browser/extension_system.h" #include "extensions/common/file_util.h" #include "extensions/common/message_bundle.h" #include "ui/base/resource/resource_bundle.h" @@ -148,12 +150,13 @@ bool UserScriptMaster::ScriptReloader::ParseMetadataHeader( void UserScriptMaster::ScriptReloader::StartLoad( const UserScriptList& user_scripts, - const ExtensionsInfo& extensions_info_) { + const ExtensionsInfo& extensions_info) { // Add a reference to ourselves to keep ourselves alive while we're running. // Balanced by NotifyMaster(). AddRef(); - this->extensions_info_ = extensions_info_; + verifier_ = master_->content_verifier(); + this->extensions_info_ = extensions_info; BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, base::Bind( @@ -175,8 +178,24 @@ void UserScriptMaster::ScriptReloader::NotifyMaster( Release(); } -static bool LoadScriptContent(UserScript::File* script_file, - const SubstitutionMap* localization_messages) { +static void VerifyContent(ContentVerifier* verifier, + const std::string& extension_id, + const base::FilePath& extension_root, + const base::FilePath& relative_path, + const std::string& content) { + scoped_refptr job( + verifier->CreateJobFor(extension_id, extension_root, relative_path)); + if (job.get()) { + job->Start(); + job->BytesRead(content.size(), content.data()); + job->DoneReading(); + } +} + +static bool LoadScriptContent(const std::string& extension_id, + UserScript::File* script_file, + const SubstitutionMap* localization_messages, + ContentVerifier* verifier) { std::string content; const base::FilePath& path = ExtensionResource::GetFilePath( script_file->extension_root(), script_file->relative_path(), @@ -199,6 +218,13 @@ static bool LoadScriptContent(UserScript::File* script_file, LOG(WARNING) << "Failed to load user script file: " << path.value(); return false; } + if (verifier) { + VerifyContent(verifier, + extension_id, + script_file->extension_root(), + script_file->relative_path(), + content); + } } // Localize the content. @@ -231,12 +257,16 @@ void UserScriptMaster::ScriptReloader::LoadUserScripts( for (size_t k = 0; k < script.js_scripts().size(); ++k) { UserScript::File& script_file = script.js_scripts()[k]; if (script_file.GetContent().empty()) - LoadScriptContent(&script_file, NULL); + LoadScriptContent( + script.extension_id(), &script_file, NULL, verifier_.get()); } for (size_t k = 0; k < script.css_scripts().size(); ++k) { UserScript::File& script_file = script.css_scripts()[k]; if (script_file.GetContent().empty()) - LoadScriptContent(&script_file, localization_messages.get()); + LoadScriptContent(script.extension_id(), + &script_file, + localization_messages.get(), + verifier_.get()); } } } @@ -370,6 +400,11 @@ void UserScriptMaster::NewScriptsAvailable(base::SharedMemory* handle) { } } +ContentVerifier* UserScriptMaster::content_verifier() { + ExtensionSystem* system = ExtensionSystem::Get(profile_); + return system->content_verifier(); +} + void UserScriptMaster::OnExtensionLoaded( content::BrowserContext* browser_context, const Extension* extension) { diff --git a/chrome/browser/extensions/user_script_master.h b/chrome/browser/extensions/user_script_master.h index 91fd3ed..909bf8b 100644 --- a/chrome/browser/extensions/user_script_master.h +++ b/chrome/browser/extensions/user_script_master.h @@ -27,6 +27,7 @@ class Profile; namespace extensions { +class ContentVerifier; class ExtensionRegistry; typedef std::map @@ -55,6 +56,9 @@ class UserScriptMaster : public base::RefCountedThreadSafe, // Return true if we have any scripts ready. bool ScriptsReady() const { return shared_memory_.get() != NULL; } + // Returns the content verifier for our browser context. + ContentVerifier* content_verifier(); + protected: friend class base::RefCountedThreadSafe; @@ -79,7 +83,7 @@ class UserScriptMaster : public base::RefCountedThreadSafe, // Start loading of scripts. // Will always send a message to the master upon completion. void StartLoad(const UserScriptList& external_scripts, - const ExtensionsInfo& extension_info_); + const ExtensionsInfo& extensions_info); // The master is going away; don't call it back. void DisownMaster() { @@ -126,6 +130,8 @@ class UserScriptMaster : public base::RefCountedThreadSafe, // Expected to always outlive us. content::BrowserThread::ID master_thread_id_; + scoped_refptr verifier_; + DISALLOW_COPY_AND_ASSIGN(ScriptReloader); }; diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index f19fa6e..47fa08e 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -1115,6 +1115,7 @@ 'browser/extensions/browsertest_util_browsertest.cc', 'browser/extensions/chrome_app_api_browsertest.cc', 'browser/extensions/chrome_ui_overrides_browsertest.cc', + 'browser/extensions/content_verifier_browsertest.cc', 'browser/extensions/content_script_apitest.cc', 'browser/extensions/content_security_policy_apitest.cc', 'browser/extensions/convert_web_app_browsertest.cc', diff --git a/chrome/test/data/extensions/content_verifier/v1.crx b/chrome/test/data/extensions/content_verifier/v1.crx new file mode 100644 index 0000000..7baacf1 Binary files /dev/null and b/chrome/test/data/extensions/content_verifier/v1.crx differ diff --git a/chrome/test/data/extensions/content_verifier/v1/manifest.json b/chrome/test/data/extensions/content_verifier/v1/manifest.json new file mode 100644 index 0000000..de8f2df --- /dev/null +++ b/chrome/test/data/extensions/content_verifier/v1/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "Test Extension", + "version": "1", + "manifest_version": 2, + "update_url": "https://clients2.google.com/service/update2/crx" +} diff --git a/chrome/test/data/extensions/content_verifier/v1/page.html b/chrome/test/data/extensions/content_verifier/v1/page.html new file mode 100644 index 0000000..3b18e51 --- /dev/null +++ b/chrome/test/data/extensions/content_verifier/v1/page.html @@ -0,0 +1 @@ +hello world diff --git a/extensions/browser/content_verifier.cc b/extensions/browser/content_verifier.cc new file mode 100644 index 0000000..879ee25 --- /dev/null +++ b/extensions/browser/content_verifier.cc @@ -0,0 +1,125 @@ +// 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_verifier.h" + +#include + +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/metrics/field_trial.h" +#include "content/public/browser/browser_thread.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/common/switches.h" + +namespace { + +const char kExperimentName[] = "ExtensionContentVerification"; + +} // namespace + +namespace extensions { + +ContentVerifier::ContentVerifier(content::BrowserContext* context, + const ContentVerifierFilter& filter) + : mode_(GetMode()), + filter_(filter), + context_(context), + observers_(new ObserverListThreadSafe) { +} + +ContentVerifier::~ContentVerifier() { +} + +void ContentVerifier::Start() { +} + +void ContentVerifier::Shutdown() { + filter_.Reset(); +} + +ContentVerifyJob* ContentVerifier::CreateJobFor( + const std::string& extension_id, + const base::FilePath& extension_root, + const base::FilePath& relative_path) { + if (filter_.is_null()) + return NULL; + + ExtensionRegistry* registry = ExtensionRegistry::Get(context_); + const Extension* extension = + registry->GetExtensionById(extension_id, ExtensionRegistry::EVERYTHING); + + if (!extension || !filter_.Run(extension)) + return NULL; + + return new ContentVerifyJob( + extension_id, + base::Bind(&ContentVerifier::VerifyFailed, this, extension->id())); +} + +void ContentVerifier::VerifyFailed(const std::string& extension_id, + ContentVerifyJob::FailureReason reason) { + if (mode_ < ENFORCE) + return; + + if (reason == ContentVerifyJob::NO_HASHES && mode_ < ENFORCE_STRICT) { + content::BrowserThread::PostTask( + content::BrowserThread::UI, + FROM_HERE, + base::Bind(&ContentVerifier::RequestFetch, this, extension_id)); + return; + } + + // The magic of ObserverListThreadSafe will make sure that observers get + // called on the same threads that they called AddObserver on. + observers_->Notify(&ContentVerifierObserver::ContentVerifyFailed, + extension_id); +} + +void ContentVerifier::AddObserver(ContentVerifierObserver* observer) { + observers_->AddObserver(observer); +} + +void ContentVerifier::RemoveObserver(ContentVerifierObserver* observer) { + observers_->RemoveObserver(observer); +} + +void ContentVerifier::RequestFetch(const std::string& extension_id) { +} + +// static +ContentVerifier::Mode ContentVerifier::GetMode() { + Mode experiment_value = NONE; + const std::string group = base::FieldTrialList::FindFullName(kExperimentName); + if (group == "EnforceStrict") + experiment_value = ENFORCE_STRICT; + else if (group == "Enforce") + experiment_value = ENFORCE; + else if (group == "Bootstrap") + experiment_value = BOOTSTRAP; + + Mode cmdline_value = NONE; + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + if (command_line->HasSwitch(switches::kExtensionContentVerification)) { + std::string switch_value = command_line->GetSwitchValueASCII( + switches::kExtensionContentVerification); + if (switch_value == switches::kExtensionContentVerificationBootstrap) + cmdline_value = BOOTSTRAP; + else if (switch_value == switches::kExtensionContentVerificationEnforce) + cmdline_value = ENFORCE; + else if (switch_value == + switches::kExtensionContentVerificationEnforceStrict) + cmdline_value = ENFORCE_STRICT; + else + // If no value was provided (or the wrong one), just default to enforce. + cmdline_value = ENFORCE; + } + + // We don't want to allow the command-line flags to eg disable enforcement if + // the experiment group says it should be on, or malware may just modify the + // command line flags. So return the more restrictive of the 2 values. + return std::max(experiment_value, cmdline_value); +} + +} // namespace extensions diff --git a/extensions/browser/content_verifier.h b/extensions/browser/content_verifier.h new file mode 100644 index 0000000..edce70a --- /dev/null +++ b/extensions/browser/content_verifier.h @@ -0,0 +1,103 @@ +// 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. + +#ifndef EXTENSIONS_BROWSER_CONTENT_VERIFIER_H_ +#define EXTENSIONS_BROWSER_CONTENT_VERIFIER_H_ + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/observer_list_threadsafe.h" +#include "extensions/browser/content_verifier_filter.h" +#include "extensions/browser/content_verify_job.h" + +namespace base { +class FilePath; +} + +namespace content { +class BrowserContext; +} + +namespace extensions { + +// Interface for clients of ContentVerifier. +class ContentVerifierObserver { + public: + // Called when the content verifier detects that a read of a file inside + // an extension did not match its expected hash. + virtual void ContentVerifyFailed(const std::string& extension_id) = 0; +}; + +// Used for managing overall content verification - both fetching content +// hashes as needed, and supplying job objects to verify file contents as they +// are read. +class ContentVerifier : public base::RefCountedThreadSafe { + public: + ContentVerifier(content::BrowserContext* context, + const ContentVerifierFilter& filter); + void Start(); + void Shutdown(); + + // Call this before reading a file within an extension. The caller owns the + // returned job. + ContentVerifyJob* CreateJobFor(const std::string& extension_id, + const base::FilePath& extension_root, + const base::FilePath& relative_path); + + // Called (typically by a verification job) to indicate that verification + // failed while reading some file in |extension_id|. + void VerifyFailed(const std::string& extension_id, + ContentVerifyJob::FailureReason reason); + + // Observers will be called back on the same thread that they call + // AddObserver on. + void AddObserver(ContentVerifierObserver* observer); + void RemoveObserver(ContentVerifierObserver* observer); + + private: + DISALLOW_COPY_AND_ASSIGN(ContentVerifier); + + friend class base::RefCountedThreadSafe; + virtual ~ContentVerifier(); + + // Attempts to fetch content hashes for |extension_id|. + void RequestFetch(const std::string& extension_id); + + // Note that it is important for these to appear in increasing "severity" + // order, because we use this to let command line flags increase, but not + // decrease, the mode you're running in compared to the experiment group. + enum Mode { + // Do not try to fetch content hashes if they are missing, and do not + // enforce them if they are present. + NONE = 0, + + // If content hashes are missing, try to fetch them, but do not enforce. + BOOTSTRAP, + + // If hashes are present, enforce them. If they are missing, try to fetch + // them. + ENFORCE, + + // Treat the absence of hashes the same as a verification failure. + ENFORCE_STRICT + }; + + static Mode GetMode(); + + // The mode we're running in - set once at creation. + const Mode mode_; + + // The filter we use to decide whether to return a ContentVerifyJob. + ContentVerifierFilter filter_; + + // The associated BrowserContext. + content::BrowserContext* context_; + + // The set of objects interested in verification failures. + scoped_refptr > observers_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_BROWSER_CONTENT_VERIFIER_H_ diff --git a/extensions/browser/content_verifier_filter.h b/extensions/browser/content_verifier_filter.h new file mode 100644 index 0000000..6327d1f --- /dev/null +++ b/extensions/browser/content_verifier_filter.h @@ -0,0 +1,23 @@ +// 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. + +#ifndef EXTENSIONS_BROWSER_CONTENT_VERIFIER_FILTER_H_ +#define EXTENSIONS_BROWSER_CONTENT_VERIFIER_FILTER_H_ + +#include "base/bind.h" +#include "base/callback.h" + +namespace extensions { + +class Extension; + +// A callback function for deciding if a given extension should have it's +// content verified or not. Returning true means "yes, it should be verified". +// +// This function should be prepared to be called on any thread. +typedef base::Callback ContentVerifierFilter; + +} // namespace extensions + +#endif // EXTENSIONS_BROWSER_CONTENT_VERIFIER_FILTER_H_ diff --git a/extensions/browser/content_verify_job.cc b/extensions/browser/content_verify_job.cc new file mode 100644 index 0000000..399f363 --- /dev/null +++ b/extensions/browser/content_verify_job.cc @@ -0,0 +1,69 @@ +// 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/stl_util.h" +#include "base/task_runner_util.h" +#include "content/public/browser/browser_thread.h" + +namespace extensions { + +namespace { + +ContentVerifyJob::TestDelegate* g_test_delegate = NULL; + +} // namespace + +ContentVerifyJob::ContentVerifyJob(const std::string& extension_id, + const FailureCallback& failure_callback) + : extension_id_(extension_id), failure_callback_(failure_callback) { + // It's ok for this object to be constructed on a different thread from where + // it's used. + thread_checker_.DetachFromThread(); +} + +ContentVerifyJob::~ContentVerifyJob() { +} + +void ContentVerifyJob::Start() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void ContentVerifyJob::BytesRead(int count, const char* data) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (g_test_delegate) { + FailureReason reason = + g_test_delegate->BytesRead(extension_id_, count, data); + if (reason != NONE) + return DispatchFailureCallback(reason); + } +} + +void ContentVerifyJob::DoneReading() { + DCHECK(thread_checker_.CalledOnValidThread()); + if (g_test_delegate) { + FailureReason reason = g_test_delegate->DoneReading(extension_id_); + if (reason != NONE) { + DispatchFailureCallback(reason); + return; + } + } +} + +// static +void ContentVerifyJob::SetDelegateForTests(TestDelegate* delegate) { + g_test_delegate = delegate; +} + +void ContentVerifyJob::DispatchFailureCallback(FailureReason reason) { + if (!failure_callback_.is_null()) { + failure_callback_.Run(reason); + failure_callback_.Reset(); + } +} + +} // namespace extensions diff --git a/extensions/browser/content_verify_job.h b/extensions/browser/content_verify_job.h new file mode 100644 index 0000000..ff75b8c --- /dev/null +++ b/extensions/browser/content_verify_job.h @@ -0,0 +1,97 @@ +// 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. + +#ifndef EXTENSIONS_BROWSER_CONTENT_VERIFY_JOB_H_ +#define EXTENSIONS_BROWSER_CONTENT_VERIFY_JOB_H_ + +#include + +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/thread_checker.h" + +namespace base { +class FilePath; +} + +namespace extensions { + +class ContentHashReader; + +// Objects of this class are responsible for verifying that the actual content +// read from an extension file matches an expected set of hashes. This class +// can be created on any thread but the rest of the methods should be called +// from only one thread. +class ContentVerifyJob : public base::RefCountedThreadSafe { + public: + enum FailureReason { + // No failure. + NONE, + + // Failed because there were no expected hashes. + NO_HASHES, + + // Some of the content read did not match the expected hash. + HASH_MISMATCH + }; + typedef base::Callback FailureCallback; + + // The |failure_callback| will be called at most once if there was a failure. + // + // IMPORTANT NOTE: this class is still a stub right now - in the future this + // constructor will also be passed information to let it lookup expected + // block hashes for the file being read. + ContentVerifyJob(const std::string& extension_id, + const FailureCallback& failure_callback); + + // This begins the process of getting expected hashes, so it should be called + // as early as possible. + void Start(); + + // Call this to add more bytes to verify. If at any point the read bytes + // don't match the expected hashes, this will dispatch the failure + // callback. The failure callback will only be run once even if more bytes + // are read. Make sure to call DoneReading so that any final bytes that were + // read that didn't align exactly on a block size boundary get their hash + // checked as well. + void BytesRead(int count, const char* data); + + // Call once when finished adding bytes via BytesRead. + void DoneReading(); + + class TestDelegate { + public: + // These methods will be called inside BytesRead/DoneReading respectively. + // If either return something other than NONE, then the failure callback + // will be dispatched with that reason. + virtual FailureReason BytesRead(const std::string& extension_id, + int count, + const char* data) = 0; + virtual FailureReason DoneReading(const std::string& extension_id) = 0; + }; + + static void SetDelegateForTests(TestDelegate* delegate); + + private: + DISALLOW_COPY_AND_ASSIGN(ContentVerifyJob); + + virtual ~ContentVerifyJob(); + friend class base::RefCountedThreadSafe; + + void DispatchFailureCallback(FailureReason reason); + + // The id of the extension for the file being verified. + std::string extension_id_; + + // Called once if verification fails. + FailureCallback failure_callback_; + + // For ensuring methods on called on the right thread. + base::ThreadChecker thread_checker_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_BROWSER_CONTENT_VERIFY_JOB_H_ diff --git a/extensions/browser/extension_protocols.cc b/extensions/browser/extension_protocols.cc index 32b6bc0..71cefcc 100644 --- a/extensions/browser/extension_protocols.cc +++ b/extensions/browser/extension_protocols.cc @@ -33,6 +33,8 @@ #include "content/public/browser/resource_request_info.h" #include "crypto/secure_hash.h" #include "crypto/sha2.h" +#include "extensions/browser/content_verifier.h" +#include "extensions/browser/content_verify_job.h" #include "extensions/browser/extensions_browser_client.h" #include "extensions/browser/info_map.h" #include "extensions/common/constants.h" @@ -165,13 +167,15 @@ class URLRequestExtensionJob : public net::URLRequestFileJob { const base::FilePath& relative_path, const std::string& content_security_policy, bool send_cors_header, - bool follow_symlinks_anywhere) + bool follow_symlinks_anywhere, + ContentVerifyJob* verify_job) : net::URLRequestFileJob( request, network_delegate, base::FilePath(), BrowserThread::GetBlockingPool()->GetTaskRunnerWithShutdownBehavior( base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)), + verify_job_(verify_job), seek_position_(0), bytes_read_(0), directory_path_(directory_path), @@ -184,13 +188,6 @@ class URLRequestExtensionJob : public net::URLRequestFileJob { if (follow_symlinks_anywhere) { resource_.set_follow_symlinks_anywhere(); } - const std::string& group = - base::FieldTrialList::FindFullName("ExtensionContentHashMeasurement"); - if (group == "Yes") { - base::ElapsedTimer timer; - hash_.reset(crypto::SecureHash::Create(crypto::SecureHash::SHA256)); - hashing_time_ = timer.Elapsed(); - } } virtual void GetResponseInfo(net::HttpResponseInfo* info) OVERRIDE { @@ -214,9 +211,25 @@ class URLRequestExtensionJob : public net::URLRequestFileJob { DCHECK(posted); } + virtual void SetExtraRequestHeaders( + const net::HttpRequestHeaders& headers) OVERRIDE { + // TODO(asargent) - we'll need to add proper support for range headers. + // crbug.com/369895. + std::string range_header; + if (headers.GetHeader(net::HttpRequestHeaders::kRange, &range_header)) { + if (verify_job_) + verify_job_ = NULL; + } + URLRequestFileJob::SetExtraRequestHeaders(headers); + } + virtual void OnSeekComplete(int64 result) OVERRIDE { DCHECK_EQ(seek_position_, 0); seek_position_ = result; + // TODO(asargent) - we'll need to add proper support for range headers. + // crbug.com/369895. + if (result > 0 && verify_job_) + verify_job_ = NULL; } virtual void OnReadComplete(net::IOBuffer* buffer, int result) OVERRIDE { @@ -227,23 +240,16 @@ class URLRequestExtensionJob : public net::URLRequestFileJob { -result); if (result > 0) { bytes_read_ += result; - if (hash_.get()) { - base::ElapsedTimer timer; - hash_->Update(buffer->data(), result); - hashing_time_ += timer.Elapsed(); + if (verify_job_) { + verify_job_->BytesRead(result, buffer->data()); + if (!remaining_bytes()) + verify_job_->DoneReading(); } } } private: virtual ~URLRequestExtensionJob() { - if (hash_.get()) { - base::ElapsedTimer timer; - std::string hash_bytes(crypto::kSHA256Length, 0); - hash_->Finish(string_as_array(&hash_bytes), hash_bytes.size()); - hashing_time_ += timer.Elapsed(); - UMA_HISTOGRAM_TIMES("ExtensionUrlRequest.HashTimeMs", hashing_time_); - } UMA_HISTOGRAM_COUNTS("ExtensionUrlRequest.TotalKbRead", bytes_read_ / 1024); UMA_HISTOGRAM_COUNTS("ExtensionUrlRequest.SeekPosition", seek_position_); } @@ -258,8 +264,7 @@ class URLRequestExtensionJob : public net::URLRequestFileJob { URLRequestFileJob::Start(); } - // A hash of the contents we've read from the file. - scoped_ptr hash_; + scoped_refptr verify_job_; // The position we seeked to in the file. int64 seek_position_; @@ -267,9 +272,6 @@ class URLRequestExtensionJob : public net::URLRequestFileJob { // The number of bytes of content we read from the file. int bytes_read_; - // Used to count the total time it takes to do hashing operations. - base::TimeDelta hashing_time_; - net::HttpResponseInfo response_info_; base::FilePath directory_path_; extensions::ExtensionResource resource_; @@ -499,6 +501,14 @@ ExtensionProtocolHandler::MaybeCreateJob( return NULL; } } + ContentVerifyJob* verify_job = NULL; + ContentVerifier* verifier = extension_info_map_->content_verifier(); + if (verifier) { + verify_job = + verifier->CreateJobFor(extension_id, directory_path, relative_path); + if (verify_job) + verify_job->Start(); + } return new URLRequestExtensionJob(request, network_delegate, @@ -507,7 +517,8 @@ ExtensionProtocolHandler::MaybeCreateJob( relative_path, content_security_policy, send_cors_header, - follow_symlinks_anywhere); + follow_symlinks_anywhere, + verify_job); } } // namespace diff --git a/extensions/browser/extension_protocols.h b/extensions/browser/extension_protocols.h index 2260374..51db6e2 100644 --- a/extensions/browser/extension_protocols.h +++ b/extensions/browser/extension_protocols.h @@ -33,7 +33,7 @@ net::HttpResponseHeaders* BuildHttpHeaders( // profiles. net::URLRequestJobFactory::ProtocolHandler* CreateExtensionProtocolHandler( bool is_incognito, - extensions::InfoMap* extension_info_map); + InfoMap* extension_info_map); } // namespace extensions diff --git a/extensions/browser/extension_system.h b/extensions/browser/extension_system.h index 1ccccdd..e658138 100644 --- a/extensions/browser/extension_system.h +++ b/extensions/browser/extension_system.h @@ -26,6 +26,7 @@ class BrowserContext; namespace extensions { class Blacklist; +class ContentVerifier; class ErrorConsole; class EventRouter; class Extension; @@ -124,6 +125,9 @@ class ExtensionSystem : public KeyedService { // Signaled when the extension system has completed its startup tasks. virtual const OneShotEvent& ready() const = 0; + + // Returns the content verifier, if any. + virtual ContentVerifier* content_verifier() = 0; }; } // namespace extensions diff --git a/extensions/browser/info_map.cc b/extensions/browser/info_map.cc index 408b4f1..42b85c5 100644 --- a/extensions/browser/info_map.cc +++ b/extensions/browser/info_map.cc @@ -5,6 +5,7 @@ #include "extensions/browser/info_map.h" #include "content/public/browser/browser_thread.h" +#include "extensions/browser/content_verifier.h" #include "extensions/common/constants.h" #include "extensions/common/extension.h" #include "extensions/common/extension_set.h" @@ -207,6 +208,10 @@ bool InfoMap::AreNotificationsDisabled( return false; } +void InfoMap::SetContentVerifier(ContentVerifier* verifier) { + content_verifier_ = verifier; +} + InfoMap::~InfoMap() { if (quota_service_) { BrowserThread::DeleteSoon( diff --git a/extensions/browser/info_map.h b/extensions/browser/info_map.h index ad47506..5dfc9c2 100644 --- a/extensions/browser/info_map.h +++ b/extensions/browser/info_map.h @@ -16,6 +16,7 @@ #include "extensions/common/extension_set.h" namespace extensions { +class ContentVerifier; class Extension; // Contains extension data that needs to be accessed on the IO thread. It can @@ -103,6 +104,9 @@ class InfoMap : public base::RefCountedThreadSafe { bool notifications_disabled); bool AreNotificationsDisabled(const std::string& extension_id) const; + void SetContentVerifier(ContentVerifier* verifier); + ContentVerifier* content_verifier() { return content_verifier_; } + private: friend class base::RefCountedThreadSafe; @@ -131,6 +135,8 @@ class InfoMap : public base::RefCountedThreadSafe { extensions::ProcessMap worker_process_map_; int signin_process_id_; + + scoped_refptr content_verifier_; }; } // namespace extensions diff --git a/extensions/common/switches.cc b/extensions/common/switches.cc index 41c3469..57ea960 100644 --- a/extensions/common/switches.cc +++ b/extensions/common/switches.cc @@ -35,6 +35,16 @@ const char kEventPageIdleTime[] = "event-page-idle-time"; // notified of its impending unload and that unload happening. const char kEventPageSuspendingTime[] = "event-page-unloading-time"; +// Values for the kExtensionContentVerification flag. See ContentVerifier::Mode +// for more explanation. +const char kExtensionContentVerificationBootstrap[] = "bootstrap"; +const char kExtensionContentVerificationEnforceStrict[] = "enforce_strict"; +const char kExtensionContentVerificationEnforce[] = "enforce"; + +// Name of the command line flag to force content verification to be on in one +// of various modes. +const char kExtensionContentVerification[] = "extension-content-verification"; + // Marks a renderer as extension process. const char kExtensionProcess[] = "extension-process"; diff --git a/extensions/common/switches.h b/extensions/common/switches.h index f79e32fe..b758403 100644 --- a/extensions/common/switches.h +++ b/extensions/common/switches.h @@ -18,6 +18,10 @@ extern const char kEnableOverrideBookmarksUI[]; extern const char kErrorConsole[]; extern const char kEventPageIdleTime[]; extern const char kEventPageSuspendingTime[]; +extern const char kExtensionContentVerificationBootstrap[]; +extern const char kExtensionContentVerificationEnforceStrict[]; +extern const char kExtensionContentVerificationEnforce[]; +extern const char kExtensionContentVerification[]; extern const char kExtensionProcess[]; extern const char kExtensionsOnChromeURLs[]; extern const char kForceDevModeHighlighting[]; diff --git a/extensions/extensions.gyp b/extensions/extensions.gyp index 5560b2f..1803028 100644 --- a/extensions/extensions.gyp +++ b/extensions/extensions.gyp @@ -312,6 +312,11 @@ 'browser/browser_context_keyed_api_factory.h', 'browser/browser_context_keyed_service_factories.cc', 'browser/browser_context_keyed_service_factories.h', + 'browser/content_verifier.cc', + 'browser/content_verifier.h', + 'browser/content_verifier_filter.h', + 'browser/content_verify_job.cc', + 'browser/content_verify_job.h', 'browser/error_map.cc', 'browser/error_map.h', 'browser/event_listener_map.cc', diff --git a/net/url_request/url_request_file_job.cc b/net/url_request/url_request_file_job.cc index 011a0da..74e6131 100644 --- a/net/url_request/url_request_file_job.cc +++ b/net/url_request/url_request_file_job.cc @@ -294,19 +294,21 @@ void URLRequestFileJob::DidSeek(int64 result) { } void URLRequestFileJob::DidRead(scoped_refptr buf, int result) { - OnReadComplete(buf.get(), result); - buf = NULL; if (result > 0) { SetStatus(URLRequestStatus()); // Clear the IO_PENDING status - } else if (result == 0) { + remaining_bytes_ -= result; + DCHECK_GE(remaining_bytes_, 0); + } + + OnReadComplete(buf.get(), result); + buf = NULL; + + if (result == 0) { NotifyDone(URLRequestStatus()); - } else { + } else if (result < 0) { NotifyDone(URLRequestStatus(URLRequestStatus::FAILED, result)); } - remaining_bytes_ -= result; - DCHECK_GE(remaining_bytes_, 0); - NotifyReadComplete(result); } diff --git a/net/url_request/url_request_file_job.h b/net/url_request/url_request_file_job.h index 3af83f5..15c1e4d 100644 --- a/net/url_request/url_request_file_job.h +++ b/net/url_request/url_request_file_job.h @@ -55,6 +55,8 @@ class NET_EXPORT URLRequestFileJob : public URLRequestJob { protected: virtual ~URLRequestFileJob(); + int64 remaining_bytes() const { return remaining_bytes_; } + // The OS-specific full path name of the file base::FilePath file_path_; -- cgit v1.1