-
-
Notifications
You must be signed in to change notification settings - Fork 144
fix: restore native tombstones and record runtime crash breadcrumbs #2007
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+376
−45
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,303 @@ | ||
| #include "CrashBreadcrumbs.h" | ||
|
|
||
| #include <android/log.h> | ||
| #include <fcntl.h> | ||
| #include <signal.h> | ||
| #include <sys/syscall.h> | ||
| #include <unistd.h> | ||
|
|
||
| #include <atomic> | ||
| #include <cstdarg> | ||
| #include <cstdio> | ||
| #include <cstring> | ||
| #include <mutex> | ||
|
|
||
| namespace { | ||
|
|
||
| constexpr size_t kMaxRuntimes = 16; | ||
| constexpr size_t kFieldMax = 160; | ||
| constexpr size_t kBufferMax = 8192; | ||
| constexpr size_t kHeaderMax = 128; | ||
|
|
||
| struct Slot { | ||
| bool used; | ||
| bool isWorker; | ||
| int runtimeId; | ||
| int tid; | ||
| char script[kFieldMax]; | ||
| char module[kFieldMax]; | ||
| }; | ||
|
|
||
| Slot g_slots[kMaxRuntimes]; | ||
| std::mutex g_mutex; | ||
|
|
||
| /* | ||
| * Rendered in two buffers alternately, so the signal handler never reads the | ||
| * one a running thread is part way through writing. | ||
| */ | ||
| char g_rendered[2][kBufferMax]; | ||
| size_t g_renderedLength[2]; | ||
| std::atomic<int> g_active{-1}; | ||
|
|
||
| std::atomic<int> g_storeFd{-1}; | ||
| std::atomic_flag g_recorded = ATOMIC_FLAG_INIT; | ||
| struct sigaction g_previous[NSIG]; | ||
|
|
||
| thread_local Slot* t_slot = nullptr; | ||
|
|
||
| int CurrentTid() { return static_cast<int>(syscall(__NR_gettid)); } | ||
|
|
||
| void CopyField(char* dst, const char* src) { | ||
| if (src == nullptr) { | ||
| dst[0] = '\0'; | ||
| return; | ||
| } | ||
| size_t length = strlen(src); | ||
| if (length < kFieldMax) { | ||
| memcpy(dst, src, length + 1); | ||
| return; | ||
| } | ||
| // Keep the tail: the file name identifies a module, the leading directories | ||
| // are shared by every module in the app. | ||
| memcpy(dst, "...", 3); | ||
| memcpy(dst + 3, src + length - (kFieldMax - 4), kFieldMax - 4); | ||
| dst[kFieldMax - 1] = '\0'; | ||
| } | ||
|
|
||
| void Append(char* out, size_t& length, const char* format, ...) | ||
| __attribute__((format(printf, 3, 4))); | ||
|
|
||
| void Append(char* out, size_t& length, const char* format, ...) { | ||
| if (length >= kBufferMax) { | ||
| return; | ||
| } | ||
| va_list args; | ||
| va_start(args, format); | ||
| int written = vsnprintf(out + length, kBufferMax - length, format, args); | ||
| va_end(args); | ||
| if (written > 0) { | ||
| length += static_cast<size_t>(written); | ||
| if (length > kBufferMax - 1) { | ||
| length = kBufferMax - 1; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void RenderLocked() { | ||
| // Once a crash is recorded the handler may be reading either buffer; a | ||
| // second flip after that point would rewrite the one it is copying out. | ||
| if (g_recorded.test(std::memory_order_acquire)) { | ||
| return; | ||
| } | ||
| int next = g_active.load(std::memory_order_relaxed) == 0 ? 1 : 0; | ||
| char* out = g_rendered[next]; | ||
| size_t length = 0; | ||
|
|
||
| Append(out, length, "NativeScript runtime state (pid %d):\n", getpid()); | ||
| for (const Slot& slot : g_slots) { | ||
| if (!slot.used) { | ||
| continue; | ||
| } | ||
| Append(out, length, " runtime=%d tid=%d %s", slot.runtimeId, slot.tid, | ||
| slot.isWorker ? "worker" : "main"); | ||
| if (slot.script[0] != '\0') { | ||
| Append(out, length, " script=%s", slot.script); | ||
| } | ||
| Append(out, length, " module=%s\n", | ||
| slot.module[0] != '\0' ? slot.module : "<none>"); | ||
| } | ||
|
|
||
| g_renderedLength[next] = length; | ||
| g_active.store(next, std::memory_order_release); | ||
| } | ||
|
|
||
| Slot* FindLocked(int runtimeId) { | ||
| for (Slot& slot : g_slots) { | ||
| if (slot.used && slot.runtimeId == runtimeId) { | ||
| return &slot; | ||
| } | ||
| } | ||
| return nullptr; | ||
| } | ||
|
|
||
| /* Async-signal-safe integer formatting; snprintf is not usable here. */ | ||
| void AppendRaw(char* out, size_t capacity, size_t& length, const char* text) { | ||
| while (*text != '\0' && length < capacity) { | ||
| out[length++] = *text++; | ||
| } | ||
| } | ||
|
|
||
| void AppendRawInt(char* out, size_t capacity, size_t& length, int value) { | ||
| char digits[16]; | ||
| size_t count = 0; | ||
| unsigned int magnitude = static_cast<unsigned int>(value); | ||
| do { | ||
| digits[count++] = static_cast<char>('0' + magnitude % 10); | ||
| magnitude /= 10; | ||
| } while (magnitude != 0 && count < sizeof(digits)); | ||
| while (count > 0 && length < capacity) { | ||
| out[length++] = digits[--count]; | ||
| } | ||
| } | ||
|
|
||
| void Handler(int signalNumber, siginfo_t* info, void* context) { | ||
| // Only the first thread to fault records; the rest are already doomed. | ||
| if (!g_recorded.test_and_set()) { | ||
| int fd = g_storeFd.load(std::memory_order_acquire); | ||
| if (fd >= 0) { | ||
| char header[kHeaderMax]; | ||
| size_t length = 0; | ||
| AppendRaw(header, sizeof(header), length, "fatal signal "); | ||
| AppendRawInt(header, sizeof(header), length, signalNumber); | ||
| AppendRaw(header, sizeof(header), length, " on tid "); | ||
| AppendRawInt(header, sizeof(header), length, CurrentTid()); | ||
| AppendRaw(header, sizeof(header), length, "\n"); | ||
|
|
||
| ssize_t written = pwrite(fd, header, length, 0); | ||
| int active = g_active.load(std::memory_order_acquire); | ||
| if (written > 0 && active >= 0) { | ||
| pwrite(fd, g_rendered[active], g_renderedLength[active], written); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| * Hand the signal to whoever owned it before us -- on Android that is | ||
| * debuggerd, which writes the tombstone. | ||
| * | ||
| * A signal the kernel raised for a real fault arrives again on its own once | ||
| * this returns and the faulting instruction re-executes, so debuggerd is | ||
| * entered with the kernel's original siginfo instead of anything | ||
| * synthesised here. One that was delivered by abort() or kill() (si_code | ||
| * <= 0) will not come back, so it has to be re-raised explicitly. | ||
| */ | ||
| sigaction(signalNumber, &g_previous[signalNumber], nullptr); | ||
| if (info == nullptr || info->si_code <= 0) { | ||
| raise(signalNumber); | ||
| } | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| namespace tns { | ||
|
|
||
| void CrashBreadcrumbs::Install() { | ||
| static std::once_flag once; | ||
| std::call_once(once, [] { | ||
| struct sigaction action = {}; | ||
| action.sa_sigaction = Handler; | ||
| // SA_ONSTACK matters for a stack-overflow SIGSEGV, which has no room left | ||
| // on the faulting stack to run a handler. bionic already gives every | ||
| // thread an alternate signal stack, so the flag is all that is needed. | ||
| action.sa_flags = SA_SIGINFO | SA_ONSTACK; | ||
| sigemptyset(&action.sa_mask); | ||
| for (int signalNumber : {SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE}) { | ||
| sigaction(signalNumber, &action, &g_previous[signalNumber]); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| void CrashBreadcrumbs::OpenStore(const std::string& filesRoot) { | ||
| static std::once_flag once; | ||
| std::call_once(once, [&filesRoot] { | ||
| std::string path = filesRoot + "/.ns-crash-breadcrumb"; | ||
| int fd = open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600); | ||
| if (fd < 0) { | ||
| return; | ||
| } | ||
|
|
||
| char previous[kBufferMax + kHeaderMax]; | ||
| ssize_t length = read(fd, previous, sizeof(previous) - 1); | ||
| if (length > 0) { | ||
| previous[length] = '\0'; | ||
| // Deliberately not ANDROID_LOG_FATAL: liblog feeds a fatal record to | ||
| // android_set_abort_message, and bionic keeps the first message it is | ||
| // given for the life of the process. Claiming that slot here would | ||
| // describe the *previous* process in this one's tombstone, and would | ||
| // shut out the abort message libc or ART writes for the real fault. | ||
| __android_log_print( | ||
| ANDROID_LOG_ERROR, "TNS.Native", | ||
| "The previous process was killed by a fatal signal. Runtime state " | ||
| "recorded at that moment (match tid against the tombstone in " | ||
| "/data/tombstones):\n%s", | ||
| previous); | ||
| ftruncate(fd, 0); | ||
| } | ||
|
|
||
| g_storeFd.store(fd, std::memory_order_release); | ||
| }); | ||
| } | ||
|
|
||
| void CrashBreadcrumbs::RegisterRuntime(int runtimeId) { | ||
| std::lock_guard<std::mutex> lock(g_mutex); | ||
| Slot* slot = FindLocked(runtimeId); | ||
| if (slot == nullptr) { | ||
| for (Slot& candidate : g_slots) { | ||
| if (!candidate.used) { | ||
| slot = &candidate; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (slot == nullptr) { | ||
| // Table full. Keep the runtimes already tracked rather than evicting one. | ||
| return; | ||
| } | ||
|
|
||
| slot->used = true; | ||
| slot->isWorker = false; | ||
| slot->runtimeId = runtimeId; | ||
| slot->tid = CurrentTid(); | ||
| slot->script[0] = '\0'; | ||
| slot->module[0] = '\0'; | ||
| t_slot = slot; | ||
| RenderLocked(); | ||
| } | ||
|
|
||
| void CrashBreadcrumbs::UnregisterRuntime(int runtimeId) { | ||
| std::lock_guard<std::mutex> lock(g_mutex); | ||
| Slot* slot = FindLocked(runtimeId); | ||
| if (slot == nullptr) { | ||
| return; | ||
| } | ||
| if (t_slot == slot) { | ||
| t_slot = nullptr; | ||
| } | ||
| slot->used = false; | ||
| RenderLocked(); | ||
| } | ||
|
|
||
| void CrashBreadcrumbs::SetWorkerScript(int runtimeId, const char* script) { | ||
| std::lock_guard<std::mutex> lock(g_mutex); | ||
| Slot* slot = FindLocked(runtimeId); | ||
| if (slot == nullptr) { | ||
| return; | ||
| } | ||
| slot->isWorker = true; | ||
| CopyField(slot->script, script); | ||
| RenderLocked(); | ||
| } | ||
|
|
||
| CrashBreadcrumbs::ModuleScope::ModuleScope(const char* modulePath) { | ||
| Slot* slot = t_slot; | ||
| if (slot == nullptr) { | ||
| return; | ||
| } | ||
| std::lock_guard<std::mutex> lock(g_mutex); | ||
| previous_ = slot->module; | ||
| restore_ = true; | ||
| CopyField(slot->module, modulePath); | ||
| RenderLocked(); | ||
| } | ||
|
|
||
| CrashBreadcrumbs::ModuleScope::~ModuleScope() { | ||
| Slot* slot = t_slot; | ||
| if (!restore_ || slot == nullptr) { | ||
| return; | ||
| } | ||
| std::lock_guard<std::mutex> lock(g_mutex); | ||
| CopyField(slot->module, previous_.c_str()); | ||
| RenderLocked(); | ||
| } | ||
|
|
||
| } // namespace tns | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| #ifndef CRASHBREADCRUMBS_H_ | ||
| #define CRASHBREADCRUMBS_H_ | ||
|
|
||
| #include <string> | ||
|
|
||
| namespace tns { | ||
|
|
||
| /* | ||
| * Records what each runtime thread was doing, so a process killed by a fatal | ||
| * signal leaves behind more than a native backtrace. | ||
| * | ||
| * The state is rendered into a plain byte buffer as it changes, on ordinary | ||
| * threads. At crash time the only work left is a write(2) of that buffer, | ||
| * which is one of the few calls POSIX permits from a signal handler -- | ||
| * anything that allocates, takes a lock or formats has already happened. | ||
| */ | ||
| class CrashBreadcrumbs { | ||
| public: | ||
| /* | ||
| * Installs SIGSEGV/SIGABRT/SIGBUS/SIGILL/SIGFPE handlers that record the | ||
| * breadcrumb and then hand the signal back to the handler installed before | ||
| * them, so debuggerd still writes the tombstone. Idempotent. | ||
| */ | ||
| static void Install(); | ||
|
|
||
| /* | ||
| * Points the store at the app's files directory and reports whatever a | ||
| * previous process left behind. Idempotent, so every runtime may call it. | ||
| */ | ||
| static void OpenStore(const std::string& filesRoot); | ||
|
|
||
| /* Binds the calling thread to a runtime for that runtime's lifetime. */ | ||
| static void RegisterRuntime(int runtimeId); | ||
| static void UnregisterRuntime(int runtimeId); | ||
|
|
||
| /* Marks a registered runtime as a worker started from `script`. */ | ||
| static void SetWorkerScript(int runtimeId, const char* script); | ||
|
|
||
| /* | ||
| * Records the module the calling runtime is executing for the lifetime of | ||
| * the scope. Module loads nest (`require` inside a module body), so the | ||
| * enclosing module is restored on destruction, on throw paths included. | ||
| */ | ||
| class ModuleScope { | ||
| public: | ||
| explicit ModuleScope(const char* modulePath); | ||
| ~ModuleScope(); | ||
| ModuleScope(const ModuleScope&) = delete; | ||
| ModuleScope& operator=(const ModuleScope&) = delete; | ||
|
|
||
| private: | ||
| std::string previous_; | ||
| bool restore_ = false; | ||
| }; | ||
| }; | ||
|
|
||
| } // namespace tns | ||
|
|
||
| #endif /* CRASHBREADCRUMBS_H_ */ |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.