bug 501034 - add a Win32 helper app to crash a process, and make automation.py use it when the app hangs. r=Waldo
authorTed Mielczarek <ted.mielczarek@gmail.com>
Fri, 16 Oct 2009 13:34:27 -0400
changeset 34284 3b932018ce6a0ee85d7e6b7ff9db23b558469967
parent 34283 c641519baa90262ccde0dcc5b09b6535d81c524c
child 34285 a9d140540b4bc859c21ac943e45dc82393d6053c
push idunknown
push userunknown
push dateunknown
reviewersWaldo
bugs501034
milestone1.9.3a1pre
bug 501034 - add a Win32 helper app to crash a process, and make automation.py use it when the app hangs. r=Waldo
allmakefiles.sh
build/automation.py.in
build/win32/Makefile.in
build/win32/crashinject.cpp
build/win32/crashinjectdll/Makefile.in
build/win32/crashinjectdll/crashinjectdll.cpp
build/win32/crashinjectdll/crashinjectdll.def
testing/mochitest/Makefile.in
--- a/allmakefiles.sh
+++ b/allmakefiles.sh
@@ -58,16 +58,17 @@ fi
 add_makefiles "
 Makefile
 build/Makefile
 build/pgo/Makefile
 build/pgo/blueprint/Makefile
 build/pgo/js-input/Makefile
 build/unix/Makefile
 build/win32/Makefile
+build/win32/crashinjectdll/Makefile
 config/Makefile
 config/autoconf.mk
 config/mkdepend/Makefile
 config/nspr/Makefile
 config/doxygen.cfg
 config/tests/src-simple/Makefile
 probes/Makefile
 extensions/Makefile
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -470,16 +470,33 @@ else:
     is received within |timeout| seconds, return a blank line.
     Returns a tuple (line, did_timeout), where |did_timeout| is True
     if the read timed out, and False otherwise."""
     (r, w, e) = select.select([f], [], [], timeout)
     if len(r) == 0:
       return ('', True)
     return (f.readline(), False)
 
+def triggerBreakpad(proc, utilityPath):
+  """Attempt to kill this process in a way that triggers Breakpad crash
+  reporting, if we know how for this platform. Otherwise just .kill() it."""
+  if CRASHREPORTER:
+    if UNIXISH:
+      # SEGV will get picked up by Breakpad's signal handler
+      os.kill(proc.pid, signal.SIGSEGV)
+      return
+    elif IS_WIN32:
+      # We should have a "crashinject" program in our utility path
+      crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
+      if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
+        return
+  #TODO: kill the process such that it triggers Breakpad on OS X
+  log.info("Can't trigger Breakpad, just killing process")
+  proc.kill()
+
 ###############
 # RUN THE APP #
 ###############
 
 def runApp(testURL, env, app, profileDir, extraArgs,
            runSSLTunnel = False, utilityPath = DIST_BIN,
            xrePath = DIST_BIN, certPath = CERTS_SRC_DIR,
            debuggerInfo = None, symbolsPath = None,
@@ -563,24 +580,17 @@ def runApp(testURL, env, app, profileDir
         logsource = stackFixerProcess.stdout
 
     (line, didTimeout) = readWithTimeout(logsource, timeout)
     while line != "" and not didTimeout:
       log.info(line.rstrip())
       (line, didTimeout) = readWithTimeout(logsource, timeout)
     if didTimeout:
       log.info("TEST-UNEXPECTED-FAIL | automation.py | application timed out after %d seconds with no output", int(timeout))
-      if CRASHREPORTER:
-        if UNIXISH:
-          os.kill(proc.pid, signal.SIGSEGV)
-        else:
-          #TODO: kill the process such that it triggers Breakpad
-          proc.kill()
-      else:
-        proc.kill()
+      triggerBreakpad(proc, utilityPath)
 
   status = proc.wait()
   if status != 0 and not didTimeout:
     log.info("TEST-UNEXPECTED-FAIL | automation.py | Exited with code %d during test run", status)
   if stackFixerProcess is not None:
     status = stackFixerProcess.wait()
     if status != 0 and not didTimeout:
       log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", status)
--- a/build/win32/Makefile.in
+++ b/build/win32/Makefile.in
@@ -37,16 +37,25 @@
 
 DEPTH = ../..
 topsrcdir = @top_srcdir@
 srcdir = @srcdir@
 VPATH = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
+ifdef ENABLE_TESTS
+DIRS += crashinjectdll
+
+PROGRAM = crashinject$(BIN_SUFFIX)
+USE_STATIC_LIBS = 1
+CPPSRCS = crashinject.cpp
+
+endif
+
 include $(topsrcdir)/config/rules.mk
 
 ifdef WIN32_REDIST_DIR
 
 ifeq (1400,$(_MSC_VER))
 REDIST_FILES = \
 	Microsoft.VC80.CRT.manifest \
 	msvcm80.dll \
new file mode 100644
--- /dev/null
+++ b/build/win32/crashinject.cpp
@@ -0,0 +1,128 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Crash Injection Utility
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Ted Mielczarek <ted.mielczarek@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Given a PID, this program attempts to inject a DLL into the process
+ * with that PID. The DLL it attempts to inject, "crashinjectdll.dll",
+ * must exist alongside this exe. The DLL will then crash the process.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <Windows.h>
+
+int main(int argc, char** argv)
+{
+  if (argc != 2) {
+    fprintf(stderr, "Usage: crashinject <PID>\n");
+    return 1;
+  }
+  
+  int pid = atoi(argv[1]);
+  if (pid <= 0) {
+    fprintf(stderr, "Usage: crashinject <PID>\n");
+    return 1;
+  }
+
+  // find our DLL to inject
+  wchar_t filename[_MAX_PATH];
+  if (GetModuleFileNameW(NULL, filename, sizeof(filename) / sizeof(wchar_t)) == 0)
+    return 1;
+
+  wchar_t* slash = wcsrchr(filename, L'\\');
+  if (slash == NULL)
+    return 1;
+
+  slash++;
+  wcscpy(slash, L"crashinjectdll.dll");
+
+  // now find our target process
+  HANDLE targetProc = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD,
+                                  FALSE,
+                                  pid);
+  if (targetProc == NULL) {
+    fprintf(stderr, "Error %d opening target process\n", GetLastError());
+    return 1;
+  }
+
+  /*
+   * This is sort of insane, but we're implementing a technique described here:
+   * http://www.codeproject.com/KB/threads/winspy.aspx#section_2
+   *
+   * The gist is to use CreateRemoteThread to create a thread in the other
+   * process, but cheat and make the thread function kernel32!LoadLibrary,
+   * so that the only remote data we have to pass to the other process
+   * is the path to the library we want to load. The library we're loading
+   * will then do its dirty work inside the other process.
+   */
+  HMODULE hKernel32 = GetModuleHandleW(L"Kernel32");
+  // allocate some memory to hold the path in the remote process
+  void*   pLibRemote = VirtualAllocEx(targetProc, NULL, sizeof(filename),
+                                      MEM_COMMIT, PAGE_READWRITE);
+  if (pLibRemote == NULL) {
+    fprintf(stderr, "Error %d in VirtualAllocEx\n", GetLastError());
+    CloseHandle(targetProc);
+    return 1;
+  }
+
+  if (!WriteProcessMemory(targetProc, pLibRemote, (void*)filename,
+                          sizeof(filename), NULL)) {
+    fprintf(stderr, "Error %d in WriteProcessMemory\n", GetLastError());
+    VirtualFreeEx(targetProc, pLibRemote, sizeof(filename), MEM_RELEASE);
+    CloseHandle(targetProc);
+    return 1;
+  }
+  // Now create a thread in the target process that will load our DLL
+  HANDLE hThread = CreateRemoteThread(
+                     targetProc, NULL, 0,
+                     (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32,
+                                                            "LoadLibraryW"),
+                     pLibRemote, 0, NULL);
+  if (hThread == NULL) {
+    fprintf(stderr, "Error %d in CreateRemoteThread\n", GetLastError());
+    VirtualFreeEx(targetProc, pLibRemote, sizeof(filename), MEM_RELEASE);
+    CloseHandle(targetProc);
+    return 1;
+  }
+  WaitForSingleObject(hThread, INFINITE);
+  // Cleanup, not that it's going to matter at this point
+  CloseHandle(hThread);
+  VirtualFreeEx(targetProc, pLibRemote, sizeof(filename), MEM_RELEASE);
+  CloseHandle(targetProc);
+  
+  return 0;
+}
new file mode 100644
--- /dev/null
+++ b/build/win32/crashinjectdll/Makefile.in
@@ -0,0 +1,53 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is Mozilla core build scripts.
+#
+# The Initial Developer of the Original Code is
+# The Mozilla Foundation
+#
+# Portions created by the Initial Developer are Copyright (C) 2009
+# the Mozilla Foundation <http://www.mozilla.org/>. All Rights Reserved.
+#
+# Contributor(s): 
+#   Ted Mielczarek <ted.mielczarek@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH = ../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+LIBRARY_NAME = crashinjectdll
+DEFFILE = $(srcdir)/crashinjectdll.def
+FORCE_SHARED_LIB = 1
+USE_STATIC_LIBS = 1
+
+CPPSRCS = crashinjectdll.cpp
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/build/win32/crashinjectdll/crashinjectdll.cpp
@@ -0,0 +1,34 @@
+#include <stdio.h>
+#include <Windows.h>
+
+// make sure we only ever spawn one thread
+DWORD tid = -1;
+
+DWORD WINAPI CrashingThread(
+  LPVOID lpParameter
+)
+{
+  // not a very friendly DLL
+  volatile int* x = (int *)0x0;
+  *x = 1;
+  return 0;
+}
+
+BOOL WINAPI DllMain(
+  HANDLE hinstDLL, 
+  DWORD dwReason, 
+  LPVOID lpvReserved
+)
+{
+  if (tid == -1)
+    // we have to crash on another thread because LoadLibrary() will
+    // catch memory access errors and return failure to the calling process
+    CreateThread( 
+                 NULL,                   // default security attributes
+                 0,                      // use default stack size  
+                 CrashingThread  ,       // thread function name
+                 NULL,                   // argument to thread function 
+                 0,                      // use default creation flags 
+                 &tid);                  // returns the thread identifier 
+  return TRUE;
+}
new file mode 100644
--- /dev/null
+++ b/build/win32/crashinjectdll/crashinjectdll.def
@@ -0,0 +1,3 @@
+LIBRARY crashinjectdll
+EXPORTS
+     DllMain
--- a/testing/mochitest/Makefile.in
+++ b/testing/mochitest/Makefile.in
@@ -93,16 +93,23 @@ libs:: $(_SERV_FILES)
 # but that we need for the test harness
 TEST_HARNESS_BINS := \
   xpcshell$(BIN_SUFFIX) \
   ssltunnel$(BIN_SUFFIX) \
   certutil$(BIN_SUFFIX) \
   pk12util$(BIN_SUFFIX) \
   $(NULL)
 
+ifeq ($(OS_ARCH),WINNT)
+TEST_HARNESS_BINS += \
+  crashinject$(BIN_SUFFIX) \
+  crashinjectdll$(DLL_SUFFIX) \
+  $(NULL)
+endif
+
 ifeq ($(OS_ARCH),Darwin)
 TEST_HARNESS_BINS += fix-macosx-stack.pl
 endif
 
 ifeq ($(OS_ARCH),Linux)
 TEST_HARNESS_BINS += fix-linux-stack.pl
 endif