[lldb/Plugins] Add ability to fetch crash information on crashed processes
authorMed Ismail Bennani <medismail.bennani@gmail.com>
Fri, 21 Feb 2020 21:43:25 +0000 (22:43 +0100)
committerMed Ismail Bennani <medismail.bennani@gmail.com>
Fri, 21 Feb 2020 21:44:36 +0000 (22:44 +0100)
Currently, in macOS, when a process crashes, lldb halts inside the
implementation disassembly without yielding any useful information.
The only way to get more information is to detach from the process, then wait
for ReportCrash to generate a report, find the report, then see what error
message was included in it. Instead of waiting for this to happen, lldb could
locate the error_string and make it available to the user.

This patch addresses this issue by enabling the user to fetch extended
crash information for crashed processes using `process status --verbose`.

Depending on the platform, this will try to gather different crash information
into an structured data dictionnary. This dictionnary is generic and extensible,
as it contains an array for each different type of crash information.

On Darwin Platforms, lldb will iterate over each of the target's images,
extract their `__crash_info` section and generated a StructuredData::Array
containing, in each entry, the module spec, its UUID, the crash messages
and the abort cause. The array will be inserted into the platform's
`m_extended_crash_info` dictionnary and `FetchExtendedCrashInformation` will
return its JSON representation like this:

```
{
  "crash-info annotations": [
    {
      "abort-cause": 0,
      "image": "/usr/lib/system/libsystem_malloc.dylib",
      "message": "main(76483,0x1000cedc0) malloc: *** error for object 0x1003040a0: pointer being freed was not allocated",
      "message2": "",
      "uuid": "5747D0C9-900D-3306-8D70-1E2EA4B7E821"
    },
    ...
  ],
  ...
}
```

This crash information can also be fetched using the SB API or lldb-rpc protocol
using SBTarget::GetExtendedCrashInformation().

rdar://37736535

Differential Revision: https://reviews.llvm.org/D74657

Signed-off-by: Med Ismail Bennani <medismail.bennani@gmail.com>
12 files changed:
lldb/bindings/interface/SBTarget.i
lldb/include/lldb/API/SBTarget.h
lldb/include/lldb/Target/Platform.h
lldb/include/lldb/Target/Process.h
lldb/source/API/SBTarget.cpp
lldb/source/Commands/CommandObjectProcess.cpp
lldb/source/Commands/Options.td
lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
lldb/test/API/functionalities/process_crash_info/Makefile [new file with mode: 0644]
lldb/test/API/functionalities/process_crash_info/TestProcessCrashInfo.py [new file with mode: 0644]
lldb/test/API/functionalities/process_crash_info/main.c [new file with mode: 0644]

index 371bf5c..aec7ab9 100644 (file)
@@ -949,6 +949,12 @@ public:
     void
     SetLaunchInfo (const lldb::SBLaunchInfo &launch_info);
 
+    %feature("autodoc", "
+    Returns the platform's process extended crash information.") GetExtendedCrashInformation;
+    lldb::SBStructuredData
+    GetExtendedCrashInformation ();
+
+
     void SetCollectingStats(bool v);
 
     bool GetCollectingStats();
index c950c12..f95c89d 100644 (file)
@@ -819,6 +819,8 @@ public:
 
   void SetLaunchInfo(const lldb::SBLaunchInfo &launch_info);
 
+  SBStructuredData GetExtendedCrashInformation();
+
 protected:
   friend class SBAddress;
   friend class SBBlock;
@@ -829,6 +831,7 @@ protected:
   friend class SBFunction;
   friend class SBInstruction;
   friend class SBModule;
+  friend class SBPlatform;
   friend class SBProcess;
   friend class SBSection;
   friend class SBSourceManager;
index 2431f94..79bbc13 100644 (file)
@@ -23,6 +23,7 @@
 #include "lldb/Utility/ArchSpec.h"
 #include "lldb/Utility/ConstString.h"
 #include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/StructuredData.h"
 #include "lldb/Utility/Timeout.h"
 #include "lldb/Utility/UserIDResolver.h"
 #include "lldb/lldb-private-forward.h"
@@ -823,6 +824,26 @@ public:
   virtual size_t ConnectToWaitingProcesses(lldb_private::Debugger &debugger,
                                            lldb_private::Status &error);
 
+  /// Gather all of crash informations into a structured data dictionnary.
+  ///
+  /// If the platform have a crashed process with crash information entries,
+  /// gather all the entries into an structured data dictionnary or return a
+  /// nullptr. This dictionnary is generic and extensible, as it contains an
+  /// array for each different type of crash information.
+  ///
+  /// \param[in] target
+  ///     The target running the crashed process.
+  ///
+  /// \return
+  ///     A structured data dictionnary containing at each entry, the crash
+  ///     information type as the entry key and the matching  an array as the
+  ///     entry value. \b nullptr if not implemented or  if the process has no
+  ///     crash information entry. \b error if an error occured.
+  virtual llvm::Expected<StructuredData::DictionarySP>
+  FetchExtendedCrashInformation(lldb_private::Target &target) {
+    return nullptr;
+  }
+
 protected:
   bool m_is_host;
   // Set to true when we are able to actually set the OS version while being
index 44f8efd..87f61c6 100644 (file)
@@ -1267,7 +1267,7 @@ public:
   ///     LLDB_INVALID_ADDRESS.
   ///
   /// \return
-  ///     A StructureDataSP object which, if non-empty, will contain the
+  ///     A StructuredDataSP object which, if non-empty, will contain the
   ///     information the DynamicLoader needs to get the initial scan of
   ///     solibs resolved.
   virtual lldb_private::StructuredData::ObjectSP
index b90e772..b07120b 100644 (file)
@@ -2388,6 +2388,30 @@ void SBTarget::SetLaunchInfo(const lldb::SBLaunchInfo &launch_info) {
     m_opaque_sp->SetProcessLaunchInfo(launch_info.ref());
 }
 
+SBStructuredData SBTarget::GetExtendedCrashInformation() {
+  LLDB_RECORD_METHOD_NO_ARGS(lldb::SBStructuredData, SBTarget,
+                             GetExtendedCrashInformation);
+  SBStructuredData data;
+  TargetSP target_sp(GetSP());
+  if (!target_sp)
+    return LLDB_RECORD_RESULT(data);
+
+  PlatformSP platform_sp = target_sp->GetPlatform();
+
+  if (!target_sp)
+    return LLDB_RECORD_RESULT(data);
+
+  auto expected_data =
+      platform_sp->FetchExtendedCrashInformation(*target_sp.get());
+
+  if (!expected_data)
+    return LLDB_RECORD_RESULT(data);
+
+  StructuredData::ObjectSP fetched_data = *expected_data;
+  data.m_impl_up->SetObjectSP(fetched_data);
+  return LLDB_RECORD_RESULT(data);
+}
+
 namespace lldb_private {
 namespace repro {
 
@@ -2630,6 +2654,8 @@ void RegisterMethods<SBTarget>(Registry &R) {
   LLDB_REGISTER_METHOD_CONST(lldb::SBLaunchInfo, SBTarget, GetLaunchInfo, ());
   LLDB_REGISTER_METHOD(void, SBTarget, SetLaunchInfo,
                        (const lldb::SBLaunchInfo &));
+  LLDB_REGISTER_METHOD(lldb::SBStructuredData, SBTarget,
+                       GetExtendedCrashInformation, ());
   LLDB_REGISTER_METHOD(
       size_t, SBTarget, ReadMemory,
       (const lldb::SBAddress, void *, size_t, lldb::SBError &));
index 0145122..4ee085e 100644 (file)
@@ -1201,6 +1201,8 @@ protected:
 
 // CommandObjectProcessStatus
 #pragma mark CommandObjectProcessStatus
+#define LLDB_OPTIONS_process_status
+#include "CommandOptions.inc"
 
 class CommandObjectProcessStatus : public CommandObjectParsed {
 public:
@@ -1209,13 +1211,57 @@ public:
             interpreter, "process status",
             "Show status and stop location for the current target process.",
             "process status",
-            eCommandRequiresProcess | eCommandTryTargetAPILock) {}
+            eCommandRequiresProcess | eCommandTryTargetAPILock),
+        m_options() {}
 
   ~CommandObjectProcessStatus() override = default;
 
+  Options *GetOptions() override { return &m_options; }
+
+  class CommandOptions : public Options {
+  public:
+    CommandOptions() : Options(), m_verbose(false) {}
+
+    ~CommandOptions() override = default;
+
+    Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
+                          ExecutionContext *execution_context) override {
+      const int short_option = m_getopt_table[option_idx].val;
+
+      switch (short_option) {
+      case 'v':
+        m_verbose = true;
+        break;
+      default:
+        llvm_unreachable("Unimplemented option");
+      }
+
+      return {};
+    }
+
+    void OptionParsingStarting(ExecutionContext *execution_context) override {
+      m_verbose = false;
+    }
+
+    llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
+      return llvm::makeArrayRef(g_process_status_options);
+    }
+
+    // Instance variables to hold the values for command options.
+    bool m_verbose;
+  };
+
+protected:
   bool DoExecute(Args &command, CommandReturnObject &result) override {
     Stream &strm = result.GetOutputStream();
     result.SetStatus(eReturnStatusSuccessFinishNoResult);
+
+    if (command.GetArgumentCount()) {
+      result.AppendError("'process status' takes no arguments");
+      result.SetStatus(eReturnStatusFailed);
+      return result.Succeeded();
+    }
+
     // No need to check "process" for validity as eCommandRequiresProcess
     // ensures it is valid
     Process *process = m_exe_ctx.GetProcessPtr();
@@ -1227,8 +1273,37 @@ public:
     process->GetStatus(strm);
     process->GetThreadStatus(strm, only_threads_with_stop_reason, start_frame,
                              num_frames, num_frames_with_source, stop_format);
+
+    if (m_options.m_verbose) {
+      PlatformSP platform_sp = process->GetTarget().GetPlatform();
+      if (!platform_sp) {
+        result.AppendError("Couldn'retrieve the target's platform");
+        result.SetStatus(eReturnStatusFailed);
+        return result.Succeeded();
+      }
+
+      auto expected_crash_info =
+          platform_sp->FetchExtendedCrashInformation(process->GetTarget());
+
+      if (!expected_crash_info) {
+        result.AppendError(llvm::toString(expected_crash_info.takeError()));
+        result.SetStatus(eReturnStatusFailed);
+        return result.Succeeded();
+      }
+
+      StructuredData::DictionarySP crash_info_sp = *expected_crash_info;
+
+      if (crash_info_sp) {
+        strm.PutCString("Extended Crash Information:\n");
+        crash_info_sp->Dump(strm);
+      }
+    }
+
     return result.Succeeded();
   }
+
+private:
+  CommandOptions m_options;
 };
 
 // CommandObjectProcessHandle
index 5512f73..1456630 100644 (file)
@@ -670,6 +670,11 @@ let Command = "process handle" in {
     Desc<"Whether or not the signal should be passed to the process.">;
 }
 
+let Command = "process status" in {
+  def process_status_verbose : Option<"verbose", "v">, Group<1>,
+    Desc<"Show verbose process status including extended crash information.">;
+}
+
 let Command = "script import" in {
   def script_import_allow_reload : Option<"allow-reload", "r">, Group<1>,
     Desc<"Allow the script to be loaded even if it was already loaded before. "
index 2a745f7..3790bd0 100644 (file)
@@ -19,6 +19,7 @@
 #include "lldb/Core/Debugger.h"
 #include "lldb/Core/Module.h"
 #include "lldb/Core/ModuleSpec.h"
+#include "lldb/Core/Section.h"
 #include "lldb/Host/Host.h"
 #include "lldb/Host/HostInfo.h"
 #include "lldb/Host/XML.h"
@@ -1501,6 +1502,129 @@ PlatformDarwin::ParseVersionBuildDir(llvm::StringRef dir) {
   return std::make_tuple(version, build);
 }
 
+llvm::Expected<StructuredData::DictionarySP>
+PlatformDarwin::FetchExtendedCrashInformation(lldb_private::Target &target) {
+  Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS));
+
+  StructuredData::ArraySP annotations = ExtractCrashInfoAnnotations(target);
+
+  if (!annotations || !annotations->GetSize()) {
+    LLDB_LOG(log, "Couldn't extract crash information annotations");
+    return nullptr;
+  }
+
+  StructuredData::DictionarySP extended_crash_info =
+      std::make_shared<StructuredData::Dictionary>();
+
+  extended_crash_info->AddItem("crash-info annotations", annotations);
+
+  return extended_crash_info;
+}
+
+StructuredData::ArraySP
+PlatformDarwin::ExtractCrashInfoAnnotations(Target &target) {
+  Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS));
+
+  ConstString section_name("__crash_info");
+  ProcessSP process_sp = target.GetProcessSP();
+  StructuredData::ArraySP array_sp = std::make_shared<StructuredData::Array>();
+
+  for (ModuleSP module : target.GetImages().Modules()) {
+    SectionList *sections = module->GetSectionList();
+
+    std::string module_name = module->GetSpecificationDescription();
+
+    // The DYDL module is skipped since it's always loaded when running the
+    // binary.
+    if (module_name == "/usr/lib/dyld")
+      continue;
+
+    if (!sections) {
+      LLDB_LOG(log, "Module {0} doesn't have any section!", module_name);
+      continue;
+    }
+
+    SectionSP crash_info = sections->FindSectionByName(section_name);
+    if (!crash_info) {
+      LLDB_LOG(log, "Module {0} doesn't have section {1}!", module_name,
+               section_name);
+      continue;
+    }
+
+    addr_t load_addr = crash_info->GetLoadBaseAddress(&target);
+
+    if (load_addr == LLDB_INVALID_ADDRESS) {
+      LLDB_LOG(log, "Module {0} has an invalid '{1}' section load address: {2}",
+               module_name, section_name, load_addr);
+      continue;
+    }
+
+    Status error;
+    CrashInfoAnnotations annotations;
+    size_t expected_size = sizeof(CrashInfoAnnotations);
+    size_t bytes_read = process_sp->ReadMemoryFromInferior(
+        load_addr, &annotations, expected_size, error);
+
+    if (expected_size != bytes_read || error.Fail()) {
+      LLDB_LOG(log, "Failed to read {0} section from memory in module {1}: {2}",
+               section_name, module_name, error);
+      continue;
+    }
+
+    // initial support added for version 5
+    if (annotations.version < 5) {
+      LLDB_LOG(log,
+               "Annotation version lower than 5 unsupported! Module {0} has "
+               "version {1} instead.",
+               module_name, annotations.version);
+      continue;
+    }
+
+    if (!annotations.message) {
+      LLDB_LOG(log, "No message available for module {0}.", module_name);
+      continue;
+    }
+
+    std::string message;
+    bytes_read =
+        process_sp->ReadCStringFromMemory(annotations.message, message, error);
+
+    if (message.empty() || bytes_read != message.size() || error.Fail()) {
+      LLDB_LOG(log, "Failed to read the message from memory in module {0}: {1}",
+               module_name, error);
+      continue;
+    }
+
+    // Remove trailing newline from message
+    if (message.back() == '\n')
+      message.pop_back();
+
+    if (!annotations.message2)
+      LLDB_LOG(log, "No message2 available for module {0}.", module_name);
+
+    std::string message2;
+    bytes_read = process_sp->ReadCStringFromMemory(annotations.message2,
+                                                   message2, error);
+
+    if (!message2.empty() && bytes_read == message2.size() && error.Success())
+      if (message2.back() == '\n')
+        message2.pop_back();
+
+    StructuredData::DictionarySP entry_sp =
+        std::make_shared<StructuredData::Dictionary>();
+
+    entry_sp->AddStringItem("image", module->GetFileSpec().GetPath(false));
+    entry_sp->AddStringItem("uuid", module->GetUUID().GetAsString());
+    entry_sp->AddStringItem("message", message);
+    entry_sp->AddStringItem("message2", message2);
+    entry_sp->AddIntegerItem("abort-cause", annotations.abort_cause);
+
+    array_sp->AddItem(entry_sp);
+  }
+
+  return array_sp;
+}
+
 void PlatformDarwin::AddClangModuleCompilationOptionsForSDKType(
     Target *target, std::vector<std::string> &options, SDKType sdk_type) {
   const std::vector<std::string> apple_arguments = {
index 28e458d..302dac4 100644 (file)
@@ -12,6 +12,7 @@
 #include "Plugins/Platform/POSIX/PlatformPOSIX.h"
 #include "lldb/Host/FileSystem.h"
 #include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/StructuredData.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/FileSystem.h"
 
@@ -84,7 +85,38 @@ public:
     iPhoneOS,
   };
 
+  llvm::Expected<lldb_private::StructuredData::DictionarySP>
+  FetchExtendedCrashInformation(lldb_private::Target &target) override;
+
 protected:
+  struct CrashInfoAnnotations {
+    uint64_t version;          // unsigned long
+    uint64_t message;          // char *
+    uint64_t signature_string; // char *
+    uint64_t backtrace;        // char *
+    uint64_t message2;         // char *
+    uint64_t thread;           // uint64_t
+    uint64_t dialog_mode;      // unsigned int
+    uint64_t abort_cause;      // unsigned int
+  };
+
+  /// Extract the `__crash_info` annotations from each of of the target's
+  /// modules.
+  ///
+  /// If the platform have a crashed processes with a `__crash_info` section,
+  /// extract the section to gather the messages annotations and the abort
+  /// cause.
+  ///
+  /// \param[in] target
+  ///     The target running the crashed process.
+  ///
+  /// \return
+  ///     A  structured data array containing at each entry in each entry, the
+  ///     module spec, its UUID, the crash messages and the abort cause.
+  ///     \b nullptr if process has no crash information annotations.
+  lldb_private::StructuredData::ArraySP
+  ExtractCrashInfoAnnotations(lldb_private::Target &target);
+
   void ReadLibdispatchOffsetsAddress(lldb_private::Process *process);
 
   void ReadLibdispatchOffsets(lldb_private::Process *process);
diff --git a/lldb/test/API/functionalities/process_crash_info/Makefile b/lldb/test/API/functionalities/process_crash_info/Makefile
new file mode 100644 (file)
index 0000000..692ba17
--- /dev/null
@@ -0,0 +1,4 @@
+C_SOURCES := main.c
+
+include Makefile.rules
+
diff --git a/lldb/test/API/functionalities/process_crash_info/TestProcessCrashInfo.py b/lldb/test/API/functionalities/process_crash_info/TestProcessCrashInfo.py
new file mode 100644 (file)
index 0000000..979efe0
--- /dev/null
@@ -0,0 +1,97 @@
+"""
+Test lldb process crash info.
+"""
+
+import os
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+class PlatformProcessCrashInfoTestCase(TestBase):
+
+    mydir = TestBase.compute_mydir(__file__)
+
+    def setUp(self):
+        TestBase.setUp(self)
+        self.runCmd("settings set auto-confirm true")
+        self.source = "main.c"
+        self.line = 3
+
+    def tearDown(self):
+        self.runCmd("settings clear auto-confirm")
+        TestBase.tearDown(self)
+
+    @skipUnlessDarwin
+    def test_cli(self):
+        """Test that `process status --verbose` fetches the extended crash
+        information dictionnary from the command-line properly."""
+        self.build()
+        exe = self.getBuildArtifact("a.out")
+        self.expect("file " + exe,
+                    patterns=["Current executable set to .*a.out"])
+
+        self.expect('process launch',
+                    patterns=["Process .* launched: .*a.out"])
+
+        self.expect('process status --verbose',
+                    patterns=["\"message\".*pointer being freed was not allocated"])
+
+
+    @skipUnlessDarwin
+    def test_api(self):
+        """Test that lldb can fetch a crashed process' extended crash information
+        dictionnary from the api properly."""
+        self.build()
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target, VALID_TARGET)
+
+        target.LaunchSimple(None, None, os.getcwd())
+
+        stream = lldb.SBStream()
+        self.assertTrue(stream)
+
+        crash_info = target.GetExtendedCrashInformation()
+
+        error = crash_info.GetAsJSON(stream)
+
+        self.assertTrue(error.Success())
+
+        self.assertTrue(crash_info.IsValid())
+
+        self.assertIn("pointer being freed was not allocated", stream.GetData())
+
+    @skipUnlessDarwin
+    def test_before_launch(self):
+        """Test that lldb doesn't fetch the extended crash information
+        dictionnary from if the process wasn't launched yet."""
+        self.build()
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target, VALID_TARGET)
+
+        stream = lldb.SBStream()
+        self.assertTrue(stream)
+
+        crash_info = target.GetExtendedCrashInformation()
+
+        error = crash_info.GetAsJSON(stream)
+        self.assertFalse(error.Success())
+        self.assertIn("No structured data.", error.GetCString())
+
+    @skipUnlessDarwin
+    def test_on_sane_process(self):
+        """Test that lldb doesn't fetch the extended crash information
+        dictionnary from a 'sane' stopped process."""
+        self.build()
+        target, _, _, _ = lldbutil.run_to_line_breakpoint(self, lldb.SBFileSpec(self.source),
+                                        self.line)
+
+        stream = lldb.SBStream()
+        self.assertTrue(stream)
+
+        crash_info = target.GetExtendedCrashInformation()
+
+        error = crash_info.GetAsJSON(stream)
+        self.assertFalse(error.Success())
+        self.assertIn("No structured data.", error.GetCString())
diff --git a/lldb/test/API/functionalities/process_crash_info/main.c b/lldb/test/API/functionalities/process_crash_info/main.c
new file mode 100644 (file)
index 0000000..4973351
--- /dev/null
@@ -0,0 +1,7 @@
+#include <stdlib.h>
+int main() {
+  int *var = malloc(sizeof(int));
+  free(var);
+  free(var);
+  return 0;
+}