1 // Copyright 2015 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 "chromecast/crash/linux/synchronized_minidump_manager.h"
6 
7 #include <errno.h>
8 #include <fcntl.h>
9 #include <stdint.h>
10 #include <string.h>
11 #include <sys/file.h>
12 #include <unistd.h>
13 
14 #include <memory>
15 #include <string>
16 #include <utility>
17 
18 #include "base/files/dir_reader_posix.h"
19 #include "base/files/file_util.h"
20 #include "base/logging.h"
21 #include "base/posix/eintr_wrapper.h"
22 #include "base/strings/string_number_conversions.h"
23 #include "base/strings/string_split.h"
24 #include "base/strings/stringprintf.h"
25 #include "chromecast/base/path_utils.h"
26 #include "chromecast/base/serializers.h"
27 #include "chromecast/crash/linux/dump_info.h"
28 
29 // if |cond| is false, returns |retval|.
30 #define RCHECK(cond, retval) \
31   do {                       \
32     if (!(cond)) {           \
33       return (retval);       \
34     }                        \
35   } while (0)
36 
37 namespace chromecast {
38 
39 namespace {
40 
41 const char kLockfileName[] = "lockfile";
42 const char kMetadataName[] = "metadata";
43 const char kMinidumpsDir[] = "minidumps";
44 
45 const char kLockfileRatelimitKey[] = "ratelimit";
46 const char kLockfileRatelimitPeriodStartKey[] = "period_start";
47 const char kLockfileRatelimitPeriodDumpsKey[] = "period_dumps";
48 const uint64_t kLockfileNumRatelimitParams = 2;
49 
50 // Gets the ratelimit parameter dictionary given a deserialized |metadata|.
51 // Returns nullptr if invalid.
GetRatelimitParams(base::Value * metadata)52 base::DictionaryValue* GetRatelimitParams(base::Value* metadata) {
53   base::DictionaryValue* dict;
54   base::DictionaryValue* ratelimit_params;
55   if (!metadata || !metadata->GetAsDictionary(&dict) ||
56       !dict->GetDictionary(kLockfileRatelimitKey, &ratelimit_params)) {
57     return nullptr;
58   }
59 
60   return ratelimit_params;
61 }
62 
63 // Returns the time of the current ratelimit period's start in |metadata|.
64 // Returns base::Time() if an error occurs.
GetRatelimitPeriodStart(base::Value * metadata)65 base::Time GetRatelimitPeriodStart(base::Value* metadata) {
66   base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
67   RCHECK(ratelimit_params, base::Time());
68 
69   double seconds = 0.0;
70   RCHECK(
71       ratelimit_params->GetDouble(kLockfileRatelimitPeriodStartKey, &seconds),
72       base::Time());
73 
74   // Return value of 0 indicates "not initialized", so we need to explicitly
75   // check for it and return time_t = 0 equivalent.
76   return seconds ? base::Time::FromDoubleT(seconds) : base::Time::UnixEpoch();
77 }
78 
79 // Sets the time of the current ratelimit period's start in |metadata| to
80 // |period_start|. Returns true on success, false on error.
SetRatelimitPeriodStart(base::Value * metadata,base::Time period_start)81 bool SetRatelimitPeriodStart(base::Value* metadata, base::Time period_start) {
82   DCHECK(!period_start.is_null());
83 
84   base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
85   RCHECK(ratelimit_params, false);
86 
87   ratelimit_params->SetDouble(kLockfileRatelimitPeriodStartKey,
88                               period_start.ToDoubleT());
89   return true;
90 }
91 
92 // Gets the number of dumps added to |metadata| in the current ratelimit
93 // period. Returns < 0 on error.
GetRatelimitPeriodDumps(base::Value * metadata)94 int GetRatelimitPeriodDumps(base::Value* metadata) {
95   int period_dumps = -1;
96 
97   base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
98   if (!ratelimit_params ||
99       !ratelimit_params->GetInteger(kLockfileRatelimitPeriodDumpsKey,
100                                     &period_dumps)) {
101     return -1;
102   }
103 
104   return period_dumps;
105 }
106 
107 // Sets the current ratelimit period's number of dumps in |metadata| to
108 // |period_dumps|. Returns true on success, false on error.
SetRatelimitPeriodDumps(base::Value * metadata,int period_dumps)109 bool SetRatelimitPeriodDumps(base::Value* metadata, int period_dumps) {
110   DCHECK_GE(period_dumps, 0);
111 
112   base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
113   RCHECK(ratelimit_params, false);
114 
115   ratelimit_params->SetInteger(kLockfileRatelimitPeriodDumpsKey, period_dumps);
116 
117   return true;
118 }
119 
120 // Returns true if |metadata| contains valid metadata, false otherwise.
ValidateMetadata(base::Value * metadata)121 bool ValidateMetadata(base::Value* metadata) {
122   RCHECK(metadata, false);
123 
124   // Validate ratelimit params
125   base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
126 
127   return ratelimit_params &&
128          ratelimit_params->size() == kLockfileNumRatelimitParams &&
129          !GetRatelimitPeriodStart(metadata).is_null() &&
130          GetRatelimitPeriodDumps(metadata) >= 0;
131 }
132 
133 // Calls flock on valid file descriptor |fd| with flag |flag|. Returns true
134 // on success, false on failure.
CallFlockOnFileWithFlag(const int fd,int flag)135 bool CallFlockOnFileWithFlag(const int fd, int flag) {
136   int ret = -1;
137   if ((ret = HANDLE_EINTR(flock(fd, flag))) < 0)
138     PLOG(ERROR) << "Error locking " << fd;
139 
140   return !ret;
141 }
142 
OpenAndLockFile(const base::FilePath & path,bool write)143 int OpenAndLockFile(const base::FilePath& path, bool write) {
144   int fd = -1;
145   const char* file = path.value().c_str();
146 
147   if ((fd = open(file, write ? O_RDWR : O_RDONLY)) < 0) {
148     PLOG(ERROR) << "Error opening " << file;
149   } else if (!CallFlockOnFileWithFlag(fd, LOCK_EX)) {
150     close(fd);
151     fd = -1;
152   }
153 
154   return fd;
155 }
156 
UnlockAndCloseFile(const int fd)157 bool UnlockAndCloseFile(const int fd) {
158   if (!CallFlockOnFileWithFlag(fd, LOCK_UN))
159     return false;
160   return !close(fd);
161 }
162 
163 }  // namespace
164 
165 // One day
166 const int SynchronizedMinidumpManager::kRatelimitPeriodSeconds = 24 * 3600;
167 const int SynchronizedMinidumpManager::kRatelimitPeriodMaxDumps = 100;
168 
SynchronizedMinidumpManager()169 SynchronizedMinidumpManager::SynchronizedMinidumpManager()
170     : dump_path_(GetHomePathASCII(kMinidumpsDir)),
171       lockfile_path_(dump_path_.Append(kLockfileName).value()),
172       metadata_path_(dump_path_.Append(kMetadataName).value()),
173       lockfile_fd_(-1) {}
174 
~SynchronizedMinidumpManager()175 SynchronizedMinidumpManager::~SynchronizedMinidumpManager() {
176   // Release the lock if held.
177   ReleaseLockFile();
178 }
179 
180 // TODO(slan): Move some of this pruning logic to ReleaseLockFile?
GetNumDumps(bool delete_all_dumps)181 int SynchronizedMinidumpManager::GetNumDumps(bool delete_all_dumps) {
182   int num_dumps = 0;
183 
184   base::DirReaderPosix reader(dump_path_.value().c_str());
185   if (!reader.IsValid()) {
186     LOG(ERROR) << "Unable to open directory " << dump_path_.value();
187     return 0;
188   }
189 
190   while (reader.Next()) {
191     if (strcmp(reader.name(), ".") == 0 || strcmp(reader.name(), "..") == 0)
192       continue;
193 
194     const base::FilePath dump_file(dump_path_.Append(reader.name()));
195     // If file cannot be found, skip.
196     if (!base::PathExists(dump_file))
197       continue;
198 
199     // Do not count |lockfile_path_| and |metadata_path_|.
200     if (lockfile_path_ != dump_file && metadata_path_ != dump_file) {
201       ++num_dumps;
202       if (delete_all_dumps) {
203         LOG(INFO) << "Removing " << reader.name()
204                   << "which was not in the lockfile";
205         if (!base::DeleteFile(dump_file))
206           PLOG(INFO) << "Removing " << dump_file.value() << " failed";
207       }
208     }
209   }
210 
211   return num_dumps;
212 }
213 
AcquireLockAndDoWork()214 bool SynchronizedMinidumpManager::AcquireLockAndDoWork() {
215   bool success = false;
216   if (AcquireLockFile()) {
217     success = DoWork();
218     ReleaseLockFile();
219   }
220   return success;
221 }
222 
AcquireLockFile()223 bool SynchronizedMinidumpManager::AcquireLockFile() {
224   DCHECK_LT(lockfile_fd_, 0);
225   // Make the directory for the minidumps if it does not exist.
226   base::File::Error error;
227   if (!CreateDirectoryAndGetError(dump_path_, &error)) {
228     LOG(ERROR) << "Failed to create directory " << dump_path_.value()
229                << ". error = " << error;
230     return false;
231   }
232 
233   // Open the lockfile. Create it if it does not exist.
234   base::File lockfile(lockfile_path_, base::File::FLAG_OPEN_ALWAYS);
235 
236   // If opening or creating the lockfile failed, we don't want to proceed
237   // with dump writing for fear of exhausting up system resources.
238   if (!lockfile.IsValid()) {
239     LOG(ERROR) << "open lockfile failed " << lockfile_path_.value();
240     return false;
241   }
242 
243   if ((lockfile_fd_ = OpenAndLockFile(lockfile_path_, false)) < 0) {
244     ReleaseLockFile();
245     return false;
246   }
247 
248   // The lockfile is open and locked. Parse it to provide subclasses with a
249   // record of all the current dumps.
250   if (!ParseFiles()) {
251     LOG(ERROR) << "Lockfile did not parse correctly. ";
252     if (!InitializeFiles() || !ParseFiles()) {
253       LOG(ERROR) << "Failed to create a new lock file!";
254       ReleaseLockFile();
255       return false;
256     }
257   }
258 
259   DCHECK(dumps_);
260   DCHECK(metadata_);
261 
262   // We successfully have acquired the lock.
263   return true;
264 }
265 
ParseFiles()266 bool SynchronizedMinidumpManager::ParseFiles() {
267   DCHECK_GE(lockfile_fd_, 0);
268   DCHECK(!dumps_);
269   DCHECK(!metadata_);
270 
271   std::string lockfile;
272   RCHECK(ReadFileToString(lockfile_path_, &lockfile), false);
273 
274   std::vector<std::string> lines = base::SplitString(
275       lockfile, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
276 
277   std::unique_ptr<base::ListValue> dumps = std::make_unique<base::ListValue>();
278 
279   // Validate dumps
280   for (const std::string& line : lines) {
281     if (line.size() == 0)
282       continue;
283     std::unique_ptr<base::Value> dump_info = DeserializeFromJson(line);
284     DumpInfo info(dump_info.get());
285     RCHECK(info.valid(), false);
286     dumps->Append(std::move(dump_info));
287   }
288 
289   std::unique_ptr<base::Value> metadata =
290       DeserializeJsonFromFile(metadata_path_);
291   RCHECK(ValidateMetadata(metadata.get()), false);
292 
293   dumps_ = std::move(dumps);
294   metadata_ = std::move(metadata);
295   return true;
296 }
297 
WriteFiles(const base::ListValue * dumps,const base::Value * metadata)298 bool SynchronizedMinidumpManager::WriteFiles(const base::ListValue* dumps,
299                                              const base::Value* metadata) {
300   DCHECK(dumps);
301   DCHECK(metadata);
302   std::string lockfile;
303 
304   for (const auto& elem : *dumps) {
305     base::Optional<std::string> dump_info = SerializeToJson(elem);
306     RCHECK(dump_info, false);
307     lockfile += *dump_info;
308     lockfile += "\n";  // Add line seperatators
309   }
310 
311   if (WriteFile(lockfile_path_, lockfile.c_str(), lockfile.size()) < 0) {
312     return false;
313   }
314 
315   return SerializeJsonToFile(metadata_path_, *metadata);
316 }
317 
InitializeFiles()318 bool SynchronizedMinidumpManager::InitializeFiles() {
319   std::unique_ptr<base::DictionaryValue> metadata =
320       std::make_unique<base::DictionaryValue>();
321 
322   auto ratelimit_fields = std::make_unique<base::DictionaryValue>();
323   ratelimit_fields->SetDouble(kLockfileRatelimitPeriodStartKey, 0.0);
324   ratelimit_fields->SetInteger(kLockfileRatelimitPeriodDumpsKey, 0);
325   metadata->Set(kLockfileRatelimitKey, std::move(ratelimit_fields));
326 
327   std::unique_ptr<base::ListValue> dumps = std::make_unique<base::ListValue>();
328 
329   return WriteFiles(dumps.get(), metadata.get());
330 }
331 
AddEntryToLockFile(const DumpInfo & dump_info)332 bool SynchronizedMinidumpManager::AddEntryToLockFile(
333     const DumpInfo& dump_info) {
334   DCHECK_GE(lockfile_fd_, 0);
335   DCHECK(dumps_);
336 
337   // Make sure dump_info is valid.
338   if (!dump_info.valid()) {
339     LOG(ERROR) << "Entry to be added is invalid";
340     return false;
341   }
342 
343   dumps_->Append(dump_info.GetAsValue());
344   return true;
345 }
346 
RemoveEntryFromLockFile(int index)347 bool SynchronizedMinidumpManager::RemoveEntryFromLockFile(int index) {
348   return dumps_->Remove(static_cast<uint64_t>(index), nullptr);
349 }
350 
ReleaseLockFile()351 void SynchronizedMinidumpManager::ReleaseLockFile() {
352   // flock is associated with the fd entry in the open fd table, so closing
353   // all fd's will release the lock. To be safe, we explicitly unlock.
354   if (lockfile_fd_ >= 0) {
355     if (dumps_ && metadata_)
356       WriteFiles(dumps_.get(), metadata_.get());
357 
358     UnlockAndCloseFile(lockfile_fd_);
359     lockfile_fd_ = -1;
360   }
361 
362   dumps_.reset();
363   metadata_.reset();
364 }
365 
GetDumps()366 std::vector<std::unique_ptr<DumpInfo>> SynchronizedMinidumpManager::GetDumps() {
367   std::vector<std::unique_ptr<DumpInfo>> dumps;
368 
369   for (const auto& elem : *dumps_) {
370     dumps.push_back(std::unique_ptr<DumpInfo>(new DumpInfo(&elem)));
371   }
372 
373   return dumps;
374 }
375 
SetCurrentDumps(const std::vector<std::unique_ptr<DumpInfo>> & dumps)376 bool SynchronizedMinidumpManager::SetCurrentDumps(
377     const std::vector<std::unique_ptr<DumpInfo>>& dumps) {
378   dumps_->Clear();
379 
380   for (auto& dump : dumps)
381     dumps_->Append(dump->GetAsValue());
382 
383   return true;
384 }
385 
IncrementNumDumpsInCurrentPeriod()386 bool SynchronizedMinidumpManager::IncrementNumDumpsInCurrentPeriod() {
387   DCHECK(metadata_);
388   int last_dumps = GetRatelimitPeriodDumps(metadata_.get());
389   RCHECK(last_dumps >= 0, false);
390 
391   return SetRatelimitPeriodDumps(metadata_.get(), last_dumps + 1);
392 }
393 
DecrementNumDumpsInCurrentPeriod()394 bool SynchronizedMinidumpManager::DecrementNumDumpsInCurrentPeriod() {
395   DCHECK(metadata_);
396   int last_dumps = GetRatelimitPeriodDumps(metadata_.get());
397   if (last_dumps > 0) {
398     return SetRatelimitPeriodDumps(metadata_.get(), last_dumps - 1);
399   }
400   return true;
401 }
402 
ResetRateLimitPeriod()403 void SynchronizedMinidumpManager::ResetRateLimitPeriod() {
404   SetRatelimitPeriodStart(metadata_.get(), base::Time::Now());
405   SetRatelimitPeriodDumps(metadata_.get(), 0);
406 }
407 
CanUploadDump()408 bool SynchronizedMinidumpManager::CanUploadDump() {
409   base::Time cur_time = base::Time::Now();
410   base::Time period_start = GetRatelimitPeriodStart(metadata_.get());
411   int period_dumps_count = GetRatelimitPeriodDumps(metadata_.get());
412 
413   // If we're in invalid state, or we passed the period, reset the ratelimit.
414   // When the device reboots, |cur_time| may be incorrectly reported to be a
415   // very small number for a short period of time. So only consider
416   // |period_start| invalid when |cur_time| is less if |cur_time| is not very
417   // close to 0.
418   if (period_dumps_count < 0 ||
419       (cur_time < period_start &&
420        cur_time.ToDoubleT() > kRatelimitPeriodSeconds) ||
421       (cur_time - period_start).InSeconds() >= kRatelimitPeriodSeconds) {
422     ResetRateLimitPeriod();
423     return true;
424   }
425 
426   return period_dumps_count < kRatelimitPeriodMaxDumps;
427 }
428 
HasDumps()429 bool SynchronizedMinidumpManager::HasDumps() {
430   // Check if lockfile has entries.
431   int64_t size = 0;
432   if (base::GetFileSize(lockfile_path_, &size) && size > 0)
433     return true;
434 
435   // Check if any files are in minidump directory
436   base::DirReaderPosix reader(dump_path_.value().c_str());
437   if (!reader.IsValid()) {
438     DLOG(ERROR) << "Could not open minidump dir: " << dump_path_.value();
439     return false;
440   }
441 
442   while (reader.Next()) {
443     if (strcmp(reader.name(), ".") == 0 || strcmp(reader.name(), "..") == 0)
444       continue;
445 
446     const base::FilePath file_path = dump_path_.Append(reader.name());
447     if (file_path != lockfile_path_ && file_path != metadata_path_)
448       return true;
449   }
450 
451   return false;
452 }
453 
InitializeFileState()454 bool SynchronizedMinidumpManager::InitializeFileState() {
455   if (!AcquireLockFile())
456     return false;  // Error logged
457 
458   ReleaseLockFile();
459   return true;
460 }
461 
462 }  // namespace chromecast
463