1// Copyright (c) 2012 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/breakpad/tools/crash_service.h"
6
7#include <windows.h>
8
9#include <sddl.h>
10#include <fstream>
11#include <map>
12
13#include "base/command_line.h"
14#include "base/file_util.h"
15#include "base/logging.h"
16#include "base/win/windows_version.h"
17#include "breakpad/src/client/windows/crash_generation/client_info.h"
18#include "breakpad/src/client/windows/crash_generation/crash_generation_server.h"
19#include "breakpad/src/client/windows/sender/crash_report_sender.h"
20
21namespace breakpad {
22
23namespace {
24
25const wchar_t kTestPipeName[] = L"\\\\.\\pipe\\ChromeCrashServices";
26
27const wchar_t kCrashReportURL[] = L"https://clients2.google.com/cr/report";
28const wchar_t kCheckPointFile[] = L"crash_checkpoint.txt";
29
30typedef std::map<std::wstring, std::wstring> CrashMap;
31
32bool CustomInfoToMap(const google_breakpad::ClientInfo* client_info,
33                     const std::wstring& reporter_tag, CrashMap* map) {
34  google_breakpad::CustomClientInfo info = client_info->GetCustomInfo();
35
36  for (uintptr_t i = 0; i < info.count; ++i) {
37    (*map)[info.entries[i].name] = info.entries[i].value;
38  }
39
40  (*map)[L"rept"] = reporter_tag;
41
42  return !map->empty();
43}
44
45bool WriteCustomInfoToFile(const std::wstring& dump_path, const CrashMap& map) {
46  std::wstring file_path(dump_path);
47  size_t last_dot = file_path.rfind(L'.');
48  if (last_dot == std::wstring::npos)
49    return false;
50  file_path.resize(last_dot);
51  file_path += L".txt";
52
53  std::wofstream file(file_path.c_str(),
54      std::ios_base::out | std::ios_base::app | std::ios::binary);
55  if (!file.is_open())
56    return false;
57
58  CrashMap::const_iterator pos;
59  for (pos = map.begin(); pos != map.end(); ++pos) {
60    std::wstring line = pos->first;
61    line += L':';
62    line += pos->second;
63    line += L'\n';
64    file.write(line.c_str(), static_cast<std::streamsize>(line.length()));
65  }
66  return true;
67}
68
69// The window procedure task is to handle when a) the user logs off.
70// b) the system shuts down or c) when the user closes the window.
71LRESULT __stdcall CrashSvcWndProc(HWND hwnd, UINT message,
72                                  WPARAM wparam, LPARAM lparam) {
73  switch (message) {
74    case WM_CLOSE:
75    case WM_ENDSESSION:
76    case WM_DESTROY:
77      PostQuitMessage(0);
78      break;
79    default:
80      return DefWindowProc(hwnd, message, wparam, lparam);
81  }
82  return 0;
83}
84
85// This is the main and only application window.
86HWND g_top_window = NULL;
87
88bool CreateTopWindow(HINSTANCE instance, bool visible) {
89  WNDCLASSEXW wcx = {0};
90  wcx.cbSize = sizeof(wcx);
91  wcx.style = CS_HREDRAW | CS_VREDRAW;
92  wcx.lpfnWndProc = CrashSvcWndProc;
93  wcx.hInstance = instance;
94  wcx.lpszClassName = L"crash_svc_class";
95  ATOM atom = ::RegisterClassExW(&wcx);
96  DWORD style = visible ? WS_POPUPWINDOW | WS_VISIBLE : WS_OVERLAPPED;
97
98  // The window size is zero but being a popup window still shows in the
99  // task bar and can be closed using the system menu or using task manager.
100  HWND window = CreateWindowExW(0, wcx.lpszClassName, L"crash service", style,
101                                CW_USEDEFAULT, CW_USEDEFAULT, 0, 0,
102                                NULL, NULL, instance, NULL);
103  if (!window)
104    return false;
105
106  ::UpdateWindow(window);
107  VLOG(1) << "window handle is " << window;
108  g_top_window = window;
109  return true;
110}
111
112// Simple helper class to keep the process alive until the current request
113// finishes.
114class ProcessingLock {
115 public:
116  ProcessingLock() {
117    ::InterlockedIncrement(&op_count_);
118  }
119  ~ProcessingLock() {
120    ::InterlockedDecrement(&op_count_);
121  }
122  static bool IsWorking() {
123    return (op_count_ != 0);
124  }
125 private:
126  static volatile LONG op_count_;
127};
128
129volatile LONG ProcessingLock::op_count_ = 0;
130
131// This structure contains the information that the worker thread needs to
132// send a crash dump to the server.
133struct DumpJobInfo {
134  DWORD pid;
135  CrashService* self;
136  CrashMap map;
137  std::wstring dump_path;
138
139  DumpJobInfo(DWORD process_id, CrashService* service,
140              const CrashMap& crash_map, const std::wstring& path)
141      : pid(process_id), self(service), map(crash_map), dump_path(path) {
142  }
143};
144
145}  // namespace
146
147// Command line switches:
148const char CrashService::kMaxReports[]        = "max-reports";
149const char CrashService::kNoWindow[]          = "no-window";
150const char CrashService::kReporterTag[]       = "reporter";
151const char CrashService::kDumpsDir[]          = "dumps-dir";
152const char CrashService::kPipeName[]          = "pipe-name";
153
154CrashService::CrashService()
155    : sender_(NULL),
156      dumper_(NULL),
157      requests_handled_(0),
158      requests_sent_(0),
159      clients_connected_(0),
160      clients_terminated_(0) {
161}
162
163CrashService::~CrashService() {
164  base::AutoLock lock(sending_);
165  delete dumper_;
166  delete sender_;
167}
168
169bool CrashService::Initialize(const base::FilePath& operating_dir,
170                              const base::FilePath& dumps_path) {
171  using google_breakpad::CrashReportSender;
172  using google_breakpad::CrashGenerationServer;
173
174  std::wstring pipe_name = kTestPipeName;
175  int max_reports = -1;
176
177  // The checkpoint file allows CrashReportSender to enforce the the maximum
178  // reports per day quota. Does not seem to serve any other purpose.
179  base::FilePath checkpoint_path = operating_dir.Append(kCheckPointFile);
180
181  CommandLine& cmd_line = *CommandLine::ForCurrentProcess();
182
183  base::FilePath dumps_path_to_use = dumps_path;
184
185  if (cmd_line.HasSwitch(kDumpsDir)) {
186    dumps_path_to_use =
187        base::FilePath(cmd_line.GetSwitchValueNative(kDumpsDir));
188  }
189
190  // We can override the send reports quota with a command line switch.
191  if (cmd_line.HasSwitch(kMaxReports))
192    max_reports = _wtoi(cmd_line.GetSwitchValueNative(kMaxReports).c_str());
193
194  // Allow the global pipe name to be overridden for better testability.
195  if (cmd_line.HasSwitch(kPipeName))
196    pipe_name = cmd_line.GetSwitchValueNative(kPipeName);
197
198#ifdef _WIN64
199  pipe_name += L"-x64";
200#endif
201
202  if (max_reports > 0) {
203    // Create the http sender object.
204    sender_ = new CrashReportSender(checkpoint_path.value());
205    sender_->set_max_reports_per_day(max_reports);
206  }
207
208  SECURITY_ATTRIBUTES security_attributes = {0};
209  SECURITY_ATTRIBUTES* security_attributes_actual = NULL;
210
211  if (base::win::GetVersion() >= base::win::VERSION_VISTA) {
212    SECURITY_DESCRIPTOR* security_descriptor =
213        reinterpret_cast<SECURITY_DESCRIPTOR*>(
214            GetSecurityDescriptorForLowIntegrity());
215    DCHECK(security_descriptor != NULL);
216
217    security_attributes.nLength = sizeof(security_attributes);
218    security_attributes.lpSecurityDescriptor = security_descriptor;
219    security_attributes.bInheritHandle = FALSE;
220
221    security_attributes_actual = &security_attributes;
222  }
223
224  // Create the OOP crash generator object.
225  dumper_ = new CrashGenerationServer(pipe_name, security_attributes_actual,
226                                      &CrashService::OnClientConnected, this,
227                                      &CrashService::OnClientDumpRequest, this,
228                                      &CrashService::OnClientExited, this,
229                                      NULL, NULL,
230                                      true, &dumps_path_to_use.value());
231
232  if (!dumper_) {
233    LOG(ERROR) << "could not create dumper";
234    if (security_attributes.lpSecurityDescriptor)
235      LocalFree(security_attributes.lpSecurityDescriptor);
236    return false;
237  }
238
239  if (!CreateTopWindow(::GetModuleHandleW(NULL),
240                       !cmd_line.HasSwitch(kNoWindow))) {
241    LOG(ERROR) << "could not create window";
242    if (security_attributes.lpSecurityDescriptor)
243      LocalFree(security_attributes.lpSecurityDescriptor);
244    return false;
245  }
246
247  reporter_tag_ = L"crash svc";
248  if (cmd_line.HasSwitch(kReporterTag))
249    reporter_tag_ = cmd_line.GetSwitchValueNative(kReporterTag);
250
251  // Log basic information.
252  VLOG(1) << "pipe name is " << pipe_name
253          << "\ndumps at " << dumps_path_to_use.value();
254
255  if (sender_) {
256    VLOG(1) << "checkpoint is " << checkpoint_path.value()
257            << "\nserver is " << kCrashReportURL
258            << "\nmaximum " << sender_->max_reports_per_day() << " reports/day"
259            << "\nreporter is " << reporter_tag_;
260  }
261  // Start servicing clients.
262  if (!dumper_->Start()) {
263    LOG(ERROR) << "could not start dumper";
264    if (security_attributes.lpSecurityDescriptor)
265      LocalFree(security_attributes.lpSecurityDescriptor);
266    return false;
267  }
268
269  if (security_attributes.lpSecurityDescriptor)
270    LocalFree(security_attributes.lpSecurityDescriptor);
271
272  // This is throwaway code. We don't need to sync with the browser process
273  // once Google Update is updated to a version supporting OOP crash handling.
274  // Create or open an event to signal the browser process that the crash
275  // service is initialized.
276  HANDLE running_event =
277      ::CreateEventW(NULL, TRUE, TRUE, L"g_chrome_crash_svc");
278  // If the browser already had the event open, the CreateEvent call did not
279  // signal it. We need to do it manually.
280  ::SetEvent(running_event);
281
282  return true;
283}
284
285void CrashService::OnClientConnected(void* context,
286    const google_breakpad::ClientInfo* client_info) {
287  ProcessingLock lock;
288  VLOG(1) << "client start. pid = " << client_info->pid();
289  CrashService* self = static_cast<CrashService*>(context);
290  ::InterlockedIncrement(&self->clients_connected_);
291}
292
293void CrashService::OnClientExited(void* context,
294    const google_breakpad::ClientInfo* client_info) {
295  ProcessingLock lock;
296  VLOG(1) << "client end. pid = " << client_info->pid();
297  CrashService* self = static_cast<CrashService*>(context);
298  ::InterlockedIncrement(&self->clients_terminated_);
299
300  if (!self->sender_)
301    return;
302
303  // When we are instructed to send reports we need to exit if there are
304  // no more clients to service. The next client that runs will start us.
305  // Only chrome.exe starts crash_service with a non-zero max_reports.
306  if (self->clients_connected_ > self->clients_terminated_)
307    return;
308  if (self->sender_->max_reports_per_day() > 0) {
309    // Wait for the other thread to send crashes, if applicable. The sender
310    // thread takes the sending_ lock, so the sleep is just to give it a
311    // chance to start.
312    ::Sleep(1000);
313    base::AutoLock lock(self->sending_);
314    // Some people can restart chrome very fast, check again if we have
315    // a new client before exiting for real.
316    if (self->clients_connected_ == self->clients_terminated_) {
317      VLOG(1) << "zero clients. exiting";
318      ::PostMessage(g_top_window, WM_CLOSE, 0, 0);
319    }
320  }
321}
322
323void CrashService::OnClientDumpRequest(void* context,
324    const google_breakpad::ClientInfo* client_info,
325    const std::wstring* file_path) {
326  ProcessingLock lock;
327
328  if (!file_path) {
329    LOG(ERROR) << "dump with no file path";
330    return;
331  }
332  if (!client_info) {
333    LOG(ERROR) << "dump with no client info";
334    return;
335  }
336
337  CrashService* self = static_cast<CrashService*>(context);
338  if (!self) {
339    LOG(ERROR) << "dump with no context";
340    return;
341  }
342
343  CrashMap map;
344  CustomInfoToMap(client_info, self->reporter_tag_, &map);
345
346  // Move dump file to the directory under client breakpad dump location.
347  base::FilePath dump_location = base::FilePath(*file_path);
348  CrashMap::const_iterator it = map.find(L"breakpad-dump-location");
349  if (it != map.end()) {
350    base::FilePath alternate_dump_location = base::FilePath(it->second);
351    base::CreateDirectoryW(alternate_dump_location);
352    alternate_dump_location = alternate_dump_location.Append(
353        dump_location.BaseName());
354    base::Move(dump_location, alternate_dump_location);
355    dump_location = alternate_dump_location;
356  }
357
358  DWORD pid = client_info->pid();
359  VLOG(1) << "dump for pid = " << pid << " is " << dump_location.value();
360
361  if (!WriteCustomInfoToFile(dump_location.value(), map)) {
362    LOG(ERROR) << "could not write custom info file";
363  }
364
365  if (!self->sender_)
366    return;
367
368  // Send the crash dump using a worker thread. This operation has retry
369  // logic in case there is no internet connection at the time.
370  DumpJobInfo* dump_job = new DumpJobInfo(pid, self, map,
371                                          dump_location.value());
372  if (!::QueueUserWorkItem(&CrashService::AsyncSendDump,
373                           dump_job, WT_EXECUTELONGFUNCTION)) {
374    LOG(ERROR) << "could not queue job";
375  }
376}
377
378// We are going to try sending the report several times. If we can't send,
379// we sleep from one minute to several hours depending on the retry round.
380unsigned long CrashService::AsyncSendDump(void* context) {
381  if (!context)
382    return 0;
383
384  DumpJobInfo* info = static_cast<DumpJobInfo*>(context);
385
386  std::wstring report_id = L"<unsent>";
387
388  const DWORD kOneMinute = 60*1000;
389  const DWORD kOneHour = 60*kOneMinute;
390
391  const DWORD kSleepSchedule[] = {
392      24*kOneHour,
393      8*kOneHour,
394      4*kOneHour,
395      kOneHour,
396      15*kOneMinute,
397      0};
398
399  int retry_round = arraysize(kSleepSchedule) - 1;
400
401  do {
402    ::Sleep(kSleepSchedule[retry_round]);
403    {
404      // Take the server lock while sending. This also prevent early
405      // termination of the service object.
406      base::AutoLock lock(info->self->sending_);
407      VLOG(1) << "trying to send report for pid = " << info->pid;
408      google_breakpad::ReportResult send_result
409          = info->self->sender_->SendCrashReport(kCrashReportURL, info->map,
410                                                 info->dump_path, &report_id);
411      switch (send_result) {
412        case google_breakpad::RESULT_FAILED:
413          report_id = L"<network issue>";
414          break;
415        case google_breakpad::RESULT_REJECTED:
416          report_id = L"<rejected>";
417          ++info->self->requests_handled_;
418          retry_round = 0;
419          break;
420        case google_breakpad::RESULT_SUCCEEDED:
421          ++info->self->requests_sent_;
422          ++info->self->requests_handled_;
423          retry_round = 0;
424          break;
425        case google_breakpad::RESULT_THROTTLED:
426          report_id = L"<throttled>";
427          break;
428        default:
429          report_id = L"<unknown>";
430          break;
431      };
432    }
433
434    VLOG(1) << "dump for pid =" << info->pid << " crash2 id =" << report_id;
435    --retry_round;
436  } while (retry_round >= 0);
437
438  if (!::DeleteFileW(info->dump_path.c_str()))
439    LOG(WARNING) << "could not delete " << info->dump_path;
440
441  delete info;
442  return 0;
443}
444
445int CrashService::ProcessingLoop() {
446  MSG msg;
447  while (GetMessage(&msg, NULL, 0, 0)) {
448    TranslateMessage(&msg);
449    DispatchMessage(&msg);
450  }
451
452  VLOG(1) << "session ending..";
453  while (ProcessingLock::IsWorking()) {
454    ::Sleep(50);
455  }
456
457  VLOG(1) << "clients connected :" << clients_connected_
458          << "\nclients terminated :" << clients_terminated_
459          << "\ndumps serviced :" << requests_handled_
460          << "\ndumps reported :" << requests_sent_;
461
462  return static_cast<int>(msg.wParam);
463}
464
465PSECURITY_DESCRIPTOR CrashService::GetSecurityDescriptorForLowIntegrity() {
466  // Build the SDDL string for the label.
467  std::wstring sddl = L"S:(ML;;NW;;;S-1-16-4096)";
468
469  DWORD error = ERROR_SUCCESS;
470  PSECURITY_DESCRIPTOR sec_desc = NULL;
471
472  PACL sacl = NULL;
473  BOOL sacl_present = FALSE;
474  BOOL sacl_defaulted = FALSE;
475
476  if (::ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl.c_str(),
477                                                             SDDL_REVISION,
478                                                             &sec_desc, NULL)) {
479    if (::GetSecurityDescriptorSacl(sec_desc, &sacl_present, &sacl,
480                                    &sacl_defaulted)) {
481      return sec_desc;
482    }
483  }
484
485  return NULL;
486}
487
488}  // namespace breakpad
489