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