nacl_browser.cc revision 0f1bc08d4cfcc34181b0b5cbf065c40f687bf740
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/nacl/browser/nacl_browser.h"
6
7#include "base/command_line.h"
8#include "base/file_util.h"
9#include "base/message_loop/message_loop.h"
10#include "base/metrics/histogram.h"
11#include "base/path_service.h"
12#include "base/pickle.h"
13#include "base/rand_util.h"
14#include "base/time/time.h"
15#include "base/win/windows_version.h"
16#include "build/build_config.h"
17#include "content/public/browser/browser_thread.h"
18#include "url/gurl.h"
19
20namespace {
21
22// An arbitrary delay to coalesce multiple writes to the cache.
23const int kValidationCacheCoalescingTimeMS = 6000;
24const char kValidationCacheSequenceName[] = "NaClValidationCache";
25const base::FilePath::CharType kValidationCacheFileName[] =
26    FILE_PATH_LITERAL("nacl_validation_cache.bin");
27
28const bool kValidationCacheEnabledByDefault = true;
29
30enum ValidationCacheStatus {
31  CACHE_MISS = 0,
32  CACHE_HIT,
33  CACHE_MAX
34};
35
36// Keep the cache bounded to an arbitrary size.  If it's too small, useful
37// entries could be evicted when multiple .nexes are loaded at once.  On the
38// other hand, entries are not always claimed (and hence removed), so the size
39// of the cache will likely saturate at its maximum size.
40// Entries may not be claimed for two main reasons. 1) the NaCl process could
41// be killed while it is loading.  2) the trusted NaCl plugin opens files using
42// the code path but doesn't resolve them.
43// TODO(ncbray) don't cache files that the plugin will not resolve.
44const int kFilePathCacheSize = 100;
45
46const base::FilePath::StringType NaClIrtName() {
47  base::FilePath::StringType irt_name(FILE_PATH_LITERAL("nacl_irt_"));
48
49#if defined(ARCH_CPU_X86_FAMILY)
50#if defined(ARCH_CPU_X86_64)
51  bool is64 = true;
52#elif defined(OS_WIN)
53  bool is64 = (base::win::OSInfo::GetInstance()->wow64_status() ==
54               base::win::OSInfo::WOW64_ENABLED);
55#else
56  bool is64 = false;
57#endif
58  if (is64)
59    irt_name.append(FILE_PATH_LITERAL("x86_64"));
60  else
61    irt_name.append(FILE_PATH_LITERAL("x86_32"));
62
63#elif defined(ARCH_CPU_ARMEL)
64  // TODO(mcgrathr): Eventually we'll need to distinguish arm32 vs thumb2.
65  // That may need to be based on the actual nexe rather than a static
66  // choice, which would require substantial refactoring.
67  irt_name.append(FILE_PATH_LITERAL("arm"));
68#elif defined(ARCH_CPU_MIPSEL)
69  irt_name.append(FILE_PATH_LITERAL("mips32"));
70#else
71#error Add support for your architecture to NaCl IRT file selection
72#endif
73  irt_name.append(FILE_PATH_LITERAL(".nexe"));
74  return irt_name;
75}
76
77bool CheckEnvVar(const char* name, bool default_value) {
78  bool result = default_value;
79  const char* var = getenv(name);
80  if (var && strlen(var) > 0) {
81    result = var[0] != '0';
82  }
83  return result;
84}
85
86void ReadCache(const base::FilePath& filename, std::string* data) {
87  if (!base::ReadFileToString(filename, data)) {
88    // Zero-size data used as an in-band error code.
89    data->clear();
90  }
91}
92
93void WriteCache(const base::FilePath& filename, const Pickle* pickle) {
94  file_util::WriteFile(filename, static_cast<const char*>(pickle->data()),
95                       pickle->size());
96}
97
98void RemoveCache(const base::FilePath& filename,
99                 const base::Closure& callback) {
100  base::DeleteFile(filename, false);
101  content::BrowserThread::PostTask(content::BrowserThread::IO, FROM_HERE,
102                                   callback);
103}
104
105void LogCacheQuery(ValidationCacheStatus status) {
106  UMA_HISTOGRAM_ENUMERATION("NaCl.ValidationCache.Query", status, CACHE_MAX);
107}
108
109void LogCacheSet(ValidationCacheStatus status) {
110  // Bucket zero is reserved for future use.
111  UMA_HISTOGRAM_ENUMERATION("NaCl.ValidationCache.Set", status, CACHE_MAX);
112}
113
114// Crash throttling parameters.
115const size_t kMaxCrashesPerInterval = 3;
116const int64 kCrashesIntervalInSeconds = 120;
117
118}  // namespace
119
120namespace nacl {
121
122void OpenNaClExecutableImpl(const base::FilePath& file_path,
123                            base::PlatformFile* file) {
124  // Get a file descriptor. On Windows, we need 'GENERIC_EXECUTE' in order to
125  // memory map the executable.
126  // IMPORTANT: This file descriptor must not have write access - that could
127  // allow a NaCl inner sandbox escape.
128  base::PlatformFileError error_code;
129  *file = base::CreatePlatformFile(
130      file_path,
131      (base::PLATFORM_FILE_OPEN |
132       base::PLATFORM_FILE_READ |
133       base::PLATFORM_FILE_EXECUTE),  // Windows only flag.
134      NULL,
135      &error_code);
136  if (error_code != base::PLATFORM_FILE_OK) {
137    *file = base::kInvalidPlatformFileValue;
138    return;
139  }
140  // Check that the file does not reference a directory. Returning a descriptor
141  // to an extension directory could allow an outer sandbox escape. openat(...)
142  // could be used to traverse into the file system.
143  base::PlatformFileInfo file_info;
144  if (!base::GetPlatformFileInfo(*file, &file_info) ||
145      file_info.is_directory) {
146    base::ClosePlatformFile(*file);
147    *file = base::kInvalidPlatformFileValue;
148    return;
149  }
150}
151
152NaClBrowser::NaClBrowser()
153    : weak_factory_(this),
154      irt_platform_file_(base::kInvalidPlatformFileValue),
155      irt_filepath_(),
156      irt_state_(NaClResourceUninitialized),
157      validation_cache_file_path_(),
158      validation_cache_is_enabled_(
159          CheckEnvVar("NACL_VALIDATION_CACHE",
160                      kValidationCacheEnabledByDefault)),
161      validation_cache_is_modified_(false),
162      validation_cache_state_(NaClResourceUninitialized),
163      path_cache_(kFilePathCacheSize),
164      ok_(true) {
165}
166
167void NaClBrowser::SetDelegate(NaClBrowserDelegate* delegate) {
168  NaClBrowser* nacl_browser = NaClBrowser::GetInstance();
169  nacl_browser->browser_delegate_.reset(delegate);
170}
171
172NaClBrowserDelegate* NaClBrowser::GetDelegate() {
173  // The delegate is not owned by the IO thread.  This accessor method can be
174  // called from other threads.
175  DCHECK(GetInstance()->browser_delegate_.get() != NULL);
176  return GetInstance()->browser_delegate_.get();
177}
178
179void NaClBrowser::EarlyStartup() {
180  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
181  InitIrtFilePath();
182  InitValidationCacheFilePath();
183}
184
185NaClBrowser::~NaClBrowser() {
186  if (irt_platform_file_ != base::kInvalidPlatformFileValue)
187    base::ClosePlatformFile(irt_platform_file_);
188}
189
190void NaClBrowser::InitIrtFilePath() {
191  // Allow the IRT library to be overridden via an environment
192  // variable.  This allows the NaCl/Chromium integration bot to
193  // specify a newly-built IRT rather than using a prebuilt one
194  // downloaded via Chromium's DEPS file.  We use the same environment
195  // variable that the standalone NaCl PPAPI plugin accepts.
196  const char* irt_path_var = getenv("NACL_IRT_LIBRARY");
197  if (irt_path_var != NULL) {
198    base::FilePath::StringType path_string(
199        irt_path_var, const_cast<const char*>(strchr(irt_path_var, '\0')));
200    irt_filepath_ = base::FilePath(path_string);
201  } else {
202    base::FilePath plugin_dir;
203    if (!browser_delegate_->GetPluginDirectory(&plugin_dir)) {
204      DLOG(ERROR) << "Failed to locate the plugins directory, NaCl disabled.";
205      MarkAsFailed();
206      return;
207    }
208    irt_filepath_ = plugin_dir.Append(NaClIrtName());
209  }
210}
211
212#if defined(OS_WIN)
213bool NaClBrowser::GetNaCl64ExePath(base::FilePath* exe_path) {
214  base::FilePath module_path;
215  if (!PathService::Get(base::FILE_MODULE, &module_path)) {
216    LOG(ERROR) << "NaCl process launch failed: could not resolve module";
217    return false;
218  }
219  *exe_path = module_path.DirName().Append(L"nacl64");
220  return true;
221}
222#endif
223
224NaClBrowser* NaClBrowser::GetInstance() {
225  return Singleton<NaClBrowser>::get();
226}
227
228bool NaClBrowser::IsReady() const {
229  return (IsOk() &&
230          irt_state_ == NaClResourceReady &&
231          validation_cache_state_ == NaClResourceReady);
232}
233
234bool NaClBrowser::IsOk() const {
235  return ok_;
236}
237
238base::PlatformFile NaClBrowser::IrtFile() const {
239  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
240  CHECK_EQ(irt_state_, NaClResourceReady);
241  CHECK_NE(irt_platform_file_, base::kInvalidPlatformFileValue);
242  return irt_platform_file_;
243}
244
245void NaClBrowser::EnsureAllResourcesAvailable() {
246  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
247  EnsureIrtAvailable();
248  EnsureValidationCacheAvailable();
249}
250
251// Load the IRT async.
252void NaClBrowser::EnsureIrtAvailable() {
253  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
254  if (IsOk() && irt_state_ == NaClResourceUninitialized) {
255    irt_state_ = NaClResourceRequested;
256    // TODO(ncbray) use blocking pool.
257    if (!base::FileUtilProxy::CreateOrOpen(
258            content::BrowserThread::GetMessageLoopProxyForThread(
259                content::BrowserThread::FILE)
260                .get(),
261            irt_filepath_,
262            base::PLATFORM_FILE_OPEN | base::PLATFORM_FILE_READ,
263            base::Bind(&NaClBrowser::OnIrtOpened,
264                       weak_factory_.GetWeakPtr()))) {
265      LOG(ERROR) << "Internal error, NaCl disabled.";
266      MarkAsFailed();
267    }
268  }
269}
270
271void NaClBrowser::OnIrtOpened(base::PlatformFileError error_code,
272                              base::PassPlatformFile file,
273                              bool created) {
274  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
275  DCHECK_EQ(irt_state_, NaClResourceRequested);
276  DCHECK(!created);
277  if (error_code == base::PLATFORM_FILE_OK) {
278    irt_platform_file_ = file.ReleaseValue();
279  } else {
280    LOG(ERROR) << "Failed to open NaCl IRT file \""
281               << irt_filepath_.LossyDisplayName()
282               << "\": " << error_code;
283    MarkAsFailed();
284  }
285  irt_state_ = NaClResourceReady;
286  CheckWaiting();
287}
288
289void NaClBrowser::FireGdbDebugStubPortOpened(int port) {
290  content::BrowserThread::PostTask(
291      content::BrowserThread::IO,
292      FROM_HERE,
293      base::Bind(debug_stub_port_listener_, port));
294}
295
296bool NaClBrowser::HasGdbDebugStubPortListener() {
297  return !debug_stub_port_listener_.is_null();
298}
299
300void NaClBrowser::SetGdbDebugStubPortListener(
301    base::Callback<void(int)> listener) {
302  debug_stub_port_listener_ = listener;
303}
304
305void NaClBrowser::ClearGdbDebugStubPortListener() {
306  debug_stub_port_listener_.Reset();
307}
308
309void NaClBrowser::InitValidationCacheFilePath() {
310  // Determine where the validation cache resides in the file system.  It
311  // exists in Chrome's cache directory and is not tied to any specific
312  // profile.
313  // Start by finding the user data directory.
314  base::FilePath user_data_dir;
315  if (!browser_delegate_->GetUserDirectory(&user_data_dir)) {
316    RunWithoutValidationCache();
317    return;
318  }
319  // The cache directory may or may not be the user data directory.
320  base::FilePath cache_file_path;
321  browser_delegate_->GetCacheDirectory(&cache_file_path);
322  // Append the base file name to the cache directory.
323
324  validation_cache_file_path_ =
325      cache_file_path.Append(kValidationCacheFileName);
326}
327
328void NaClBrowser::EnsureValidationCacheAvailable() {
329  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
330  if (IsOk() && validation_cache_state_ == NaClResourceUninitialized) {
331    if (ValidationCacheIsEnabled()) {
332      validation_cache_state_ = NaClResourceRequested;
333
334      // Structure for carrying data between the callbacks.
335      std::string* data = new std::string();
336      // We can get away not giving this a sequence ID because this is the first
337      // task and further file access will not occur until after we get a
338      // response.
339      if (!content::BrowserThread::PostBlockingPoolTaskAndReply(
340              FROM_HERE,
341              base::Bind(ReadCache, validation_cache_file_path_, data),
342              base::Bind(&NaClBrowser::OnValidationCacheLoaded,
343                         weak_factory_.GetWeakPtr(),
344                         base::Owned(data)))) {
345        RunWithoutValidationCache();
346      }
347    } else {
348      RunWithoutValidationCache();
349    }
350  }
351}
352
353void NaClBrowser::OnValidationCacheLoaded(const std::string *data) {
354  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
355  // Did the cache get cleared before the load completed?  If so, ignore the
356  // incoming data.
357  if (validation_cache_state_ == NaClResourceReady)
358    return;
359
360  if (data->size() == 0) {
361    // No file found.
362    validation_cache_.Reset();
363  } else {
364    Pickle pickle(data->data(), data->size());
365    validation_cache_.Deserialize(&pickle);
366  }
367  validation_cache_state_ = NaClResourceReady;
368  CheckWaiting();
369}
370
371void NaClBrowser::RunWithoutValidationCache() {
372  // Be paranoid.
373  validation_cache_.Reset();
374  validation_cache_is_enabled_ = false;
375  validation_cache_state_ = NaClResourceReady;
376  CheckWaiting();
377}
378
379void NaClBrowser::CheckWaiting() {
380  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
381  if (!IsOk() || IsReady()) {
382    // Queue the waiting tasks into the message loop.  This helps avoid
383    // re-entrancy problems that could occur if the closure was invoked
384    // directly.  For example, this could result in use-after-free of the
385    // process host.
386    for (std::vector<base::Closure>::iterator iter = waiting_.begin();
387         iter != waiting_.end(); ++iter) {
388      base::MessageLoop::current()->PostTask(FROM_HERE, *iter);
389    }
390    waiting_.clear();
391  }
392}
393
394void NaClBrowser::MarkAsFailed() {
395  ok_ = false;
396  CheckWaiting();
397}
398
399void NaClBrowser::WaitForResources(const base::Closure& reply) {
400  waiting_.push_back(reply);
401  EnsureAllResourcesAvailable();
402  CheckWaiting();
403}
404
405const base::FilePath& NaClBrowser::GetIrtFilePath() {
406  return irt_filepath_;
407}
408
409void NaClBrowser::PutFilePath(const base::FilePath& path, uint64* file_token_lo,
410                              uint64* file_token_hi) {
411  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
412  while (true) {
413    uint64 file_token[2] = {base::RandUint64(), base::RandUint64()};
414    // A zero file_token indicates there is no file_token, if we get zero, ask
415    // for another number.
416    if (file_token[0] != 0 || file_token[1] != 0) {
417      // If the file_token is in use, ask for another number.
418      std::string key(reinterpret_cast<char*>(file_token), sizeof(file_token));
419      PathCacheType::iterator iter = path_cache_.Peek(key);
420      if (iter == path_cache_.end()) {
421        path_cache_.Put(key, path);
422        *file_token_lo = file_token[0];
423        *file_token_hi = file_token[1];
424        break;
425      }
426    }
427  }
428}
429
430bool NaClBrowser::GetFilePath(uint64 file_token_lo, uint64 file_token_hi,
431                              base::FilePath* path) {
432  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
433  uint64 file_token[2] = {file_token_lo, file_token_hi};
434  std::string key(reinterpret_cast<char*>(file_token), sizeof(file_token));
435  PathCacheType::iterator iter = path_cache_.Peek(key);
436  if (iter == path_cache_.end()) {
437    *path = base::FilePath(FILE_PATH_LITERAL(""));
438    return false;
439  }
440  *path = iter->second;
441  path_cache_.Erase(iter);
442  return true;
443}
444
445
446bool NaClBrowser::QueryKnownToValidate(const std::string& signature,
447                                       bool off_the_record) {
448  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
449  if (off_the_record) {
450    // If we're off the record, don't reorder the main cache.
451    return validation_cache_.QueryKnownToValidate(signature, false) ||
452        off_the_record_validation_cache_.QueryKnownToValidate(signature, true);
453  } else {
454    bool result = validation_cache_.QueryKnownToValidate(signature, true);
455    LogCacheQuery(result ? CACHE_HIT : CACHE_MISS);
456    // Queries can modify the MRU order of the cache.
457    MarkValidationCacheAsModified();
458    return result;
459  }
460}
461
462void NaClBrowser::SetKnownToValidate(const std::string& signature,
463                                     bool off_the_record) {
464  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
465  if (off_the_record) {
466    off_the_record_validation_cache_.SetKnownToValidate(signature);
467  } else {
468    validation_cache_.SetKnownToValidate(signature);
469    // The number of sets should be equal to the number of cache misses, minus
470    // validation failures and successful validations where stubout occurs.
471    LogCacheSet(CACHE_HIT);
472    MarkValidationCacheAsModified();
473  }
474}
475
476void NaClBrowser::ClearValidationCache(const base::Closure& callback) {
477  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
478  // Note: this method may be called before EnsureValidationCacheAvailable has
479  // been invoked.  In other words, this method may be called before any NaCl
480  // processes have been created.  This method must succeed and invoke the
481  // callback in such a case.  If it does not invoke the callback, Chrome's UI
482  // will hang in that case.
483  validation_cache_.Reset();
484  off_the_record_validation_cache_.Reset();
485
486  if (validation_cache_file_path_.empty()) {
487    // Can't figure out what file to remove, but don't drop the callback.
488    content::BrowserThread::PostTask(content::BrowserThread::IO, FROM_HERE,
489                                     callback);
490  } else {
491    // Delegate the removal of the cache from the filesystem to another thread
492    // to avoid blocking the IO thread.
493    // This task is dispatched immediately, not delayed and coalesced, because
494    // the user interface for cache clearing is likely waiting for the callback.
495    // In addition, we need to make sure the cache is actually cleared before
496    // invoking the callback to meet the implicit guarantees of the UI.
497    content::BrowserThread::PostBlockingPoolSequencedTask(
498        kValidationCacheSequenceName,
499        FROM_HERE,
500        base::Bind(RemoveCache, validation_cache_file_path_, callback));
501  }
502
503  // Make sure any delayed tasks to persist the cache to the filesystem are
504  // squelched.
505  validation_cache_is_modified_ = false;
506
507  // If the cache is cleared before it is loaded from the filesystem, act as if
508  // we just loaded an empty cache.
509  if (validation_cache_state_ != NaClResourceReady) {
510    validation_cache_state_ = NaClResourceReady;
511    CheckWaiting();
512  }
513}
514
515void NaClBrowser::MarkValidationCacheAsModified() {
516  if (!validation_cache_is_modified_) {
517    // Wait before persisting to disk.  This can coalesce multiple cache
518    // modifications info a single disk write.
519    base::MessageLoop::current()->PostDelayedTask(
520         FROM_HERE,
521         base::Bind(&NaClBrowser::PersistValidationCache,
522                    weak_factory_.GetWeakPtr()),
523         base::TimeDelta::FromMilliseconds(kValidationCacheCoalescingTimeMS));
524    validation_cache_is_modified_ = true;
525  }
526}
527
528void NaClBrowser::PersistValidationCache() {
529  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
530  // validation_cache_is_modified_ may be false if the cache was cleared while
531  // this delayed task was pending.
532  // validation_cache_file_path_ may be empty if something went wrong during
533  // initialization.
534  if (validation_cache_is_modified_ && !validation_cache_file_path_.empty()) {
535    Pickle* pickle = new Pickle();
536    validation_cache_.Serialize(pickle);
537
538    // Pass the serialized data to another thread to write to disk.  File IO is
539    // not allowed on the IO thread (which is the thread this method runs on)
540    // because it can degrade the responsiveness of the browser.
541    // The task is sequenced so that multiple writes happen in order.
542    content::BrowserThread::PostBlockingPoolSequencedTask(
543        kValidationCacheSequenceName,
544        FROM_HERE,
545        base::Bind(WriteCache, validation_cache_file_path_,
546                   base::Owned(pickle)));
547  }
548  validation_cache_is_modified_ = false;
549}
550
551void NaClBrowser::OnProcessCrashed() {
552  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
553  if (crash_times_.size() == kMaxCrashesPerInterval) {
554    crash_times_.pop_front();
555  }
556  base::Time time = base::Time::Now();
557  crash_times_.push_back(time);
558}
559
560bool NaClBrowser::IsThrottled() {
561  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
562  if (crash_times_.size() != kMaxCrashesPerInterval) {
563    return false;
564  }
565  base::TimeDelta delta = base::Time::Now() - crash_times_.front();
566  return delta.InSeconds() <= kCrashesIntervalInSeconds;
567}
568
569}  // namespace nacl
570