Skip to content

Commit c3fbe51

Browse files
committed
fix: restore native tombstones and record runtime crash breadcrumbs
SIG_handler was installed for SIGABRT and SIGSEGV and threw a NativeScriptException from inside the signal handler. Throwing a C++ exception from a signal handler is undefined behaviour: the unwinder cannot cross the kernel-built signal frame, so on arm64 the throw reached std::terminate -> LogAndAbortUncaught -> _Exit(EXIT_FAILURE) and the process died anyway. Installing sa_handler for SIGSEGV also displaced debuggerd, so no tombstone was written - no backtrace, no registers, no fault address. A crash produced four lines of logcat and nothing else. Replace it with a diagnostic-only handler that never throws. It records a pre-rendered breadcrumb with a single write(2) and hands the signal back to the handler that owned it before, so debuggerd still writes the tombstone with the kernel's original siginfo. The breadcrumb names every live runtime by id, tid, main-vs-worker, worker script and the module it last entered, recovering the identity the tombstone truncates to 15 characters of thread name. It is rendered on ordinary threads whenever it changes, so the handler allocates nothing, takes no lock, and calls no JNI, V8 or logging code. android_set_abort_message is deliberately not used: bionic keeps the first message it is given, so it cannot carry state that changes, and claiming the slot would shut out the abort message libc or ART writes for the real fault. std::set_terminate(LogAndAbortUncaught) is unchanged - it is the legitimate handler for genuine uncaught C++ exceptions and already _Exit()s. Fatal signals now surface as real native crashes instead of being converted into JS exceptions. The disabled exceptionHandlingTests spec that asserted a JNI misuse yields a catchable "SIGABRT" exception is removed along with the behaviour it documented.
1 parent c26048c commit c3fbe51

7 files changed

Lines changed: 345 additions & 45 deletions

File tree

‎test-app/app/src/main/assets/app/tests/exceptionHandlingTests.js‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -319,19 +319,4 @@ describe("Tests exception handling ", function () {
319319
expect(errMsg).toContain("SyntaxError: Unexpected token 'class'");
320320
expect(errMsg).toContain("File: (file:///data/data/com.tns.testapplication/files/app/tests/syntaxErrors.js:3:4)");
321321
});
322-
323-
// run this test only for API level bigger than 25 as we have handling there
324-
if(android.os.Build.VERSION.SDK_INT > 25 && android.os.Build.CPU_ABI != "x86" && android.os.Build.CPU_ABI != "x86_64") {
325-
xit("Should handle SIGABRT and throw a NativeScript exception when incorrectly calling JNI methods", function () {
326-
let myClassInstance = new com.tns.tests.MyTestBaseClass3();
327-
// public void callMeWithAString(java.lang.String[] stringArr, Runnable arbitraryInterface)
328-
try {
329-
myClassInstance.callMeWithAString("stringVal", new java.lang.Runnable({ run: () => {} }))
330-
} catch (e) {
331-
android.util.Log.d("~~~~~", "~~~~~~~~ " + e.toString());
332-
333-
expect(e.toString()).toContain("SIGABRT");
334-
}
335-
});
336-
}
337322
});

‎test-app/runtime/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ add_library(
167167
src/main/cpp/CallbackHandlers.cpp
168168
src/main/cpp/ConcurrentQueue.cpp
169169
src/main/cpp/Constants.cpp
170+
src/main/cpp/CrashBreadcrumbs.cpp
170171
src/main/cpp/DirectBuffer.cpp
171172
src/main/cpp/ErrorEvents.cpp
172173
src/main/cpp/EventLoop.cpp
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
#include "CrashBreadcrumbs.h"
2+
3+
#include <android/log.h>
4+
#include <fcntl.h>
5+
#include <signal.h>
6+
#include <sys/syscall.h>
7+
#include <unistd.h>
8+
9+
#include <atomic>
10+
#include <cstdarg>
11+
#include <cstdio>
12+
#include <cstring>
13+
#include <mutex>
14+
15+
namespace {
16+
17+
constexpr size_t kMaxRuntimes = 16;
18+
constexpr size_t kFieldMax = 160;
19+
constexpr size_t kBufferMax = 8192;
20+
constexpr size_t kHeaderMax = 128;
21+
22+
struct Slot {
23+
bool used;
24+
bool isWorker;
25+
int runtimeId;
26+
int tid;
27+
char script[kFieldMax];
28+
char module[kFieldMax];
29+
};
30+
31+
Slot g_slots[kMaxRuntimes];
32+
std::mutex g_mutex;
33+
34+
/*
35+
* Rendered in two buffers alternately, so the signal handler never reads the
36+
* one a running thread is part way through writing.
37+
*/
38+
char g_rendered[2][kBufferMax];
39+
size_t g_renderedLength[2];
40+
std::atomic<int> g_active{-1};
41+
42+
int g_storeFd = -1;
43+
std::atomic_flag g_recorded = ATOMIC_FLAG_INIT;
44+
struct sigaction g_previous[NSIG];
45+
46+
thread_local Slot* t_slot = nullptr;
47+
48+
int CurrentTid() { return static_cast<int>(syscall(__NR_gettid)); }
49+
50+
void CopyField(char* dst, const char* src) {
51+
if (src == nullptr) {
52+
dst[0] = '\0';
53+
return;
54+
}
55+
size_t length = strlen(src);
56+
if (length < kFieldMax) {
57+
memcpy(dst, src, length + 1);
58+
return;
59+
}
60+
// Keep the tail: the file name identifies a module, the leading directories
61+
// are shared by every module in the app.
62+
memcpy(dst, "...", 3);
63+
memcpy(dst + 3, src + length - (kFieldMax - 4), kFieldMax - 4);
64+
dst[kFieldMax - 1] = '\0';
65+
}
66+
67+
void Append(char* out, size_t& length, const char* format, ...)
68+
__attribute__((format(printf, 3, 4)));
69+
70+
void Append(char* out, size_t& length, const char* format, ...) {
71+
if (length >= kBufferMax) {
72+
return;
73+
}
74+
va_list args;
75+
va_start(args, format);
76+
int written = vsnprintf(out + length, kBufferMax - length, format, args);
77+
va_end(args);
78+
if (written > 0) {
79+
length += static_cast<size_t>(written);
80+
if (length > kBufferMax - 1) {
81+
length = kBufferMax - 1;
82+
}
83+
}
84+
}
85+
86+
void RenderLocked() {
87+
int next = g_active.load(std::memory_order_relaxed) == 0 ? 1 : 0;
88+
char* out = g_rendered[next];
89+
size_t length = 0;
90+
91+
Append(out, length, "NativeScript runtime state (pid %d):\n", getpid());
92+
for (const Slot& slot : g_slots) {
93+
if (!slot.used) {
94+
continue;
95+
}
96+
Append(out, length, " runtime=%d tid=%d %s", slot.runtimeId, slot.tid,
97+
slot.isWorker ? "worker" : "main");
98+
if (slot.script[0] != '\0') {
99+
Append(out, length, " script=%s", slot.script);
100+
}
101+
Append(out, length, " module=%s\n",
102+
slot.module[0] != '\0' ? slot.module : "<none>");
103+
}
104+
105+
g_renderedLength[next] = length;
106+
g_active.store(next, std::memory_order_release);
107+
}
108+
109+
Slot* FindLocked(int runtimeId) {
110+
for (Slot& slot : g_slots) {
111+
if (slot.used && slot.runtimeId == runtimeId) {
112+
return &slot;
113+
}
114+
}
115+
return nullptr;
116+
}
117+
118+
/* Async-signal-safe integer formatting; snprintf is not usable here. */
119+
void AppendRaw(char* out, size_t capacity, size_t& length, const char* text) {
120+
while (*text != '\0' && length < capacity) {
121+
out[length++] = *text++;
122+
}
123+
}
124+
125+
void AppendRawInt(char* out, size_t capacity, size_t& length, int value) {
126+
char digits[16];
127+
size_t count = 0;
128+
unsigned int magnitude = static_cast<unsigned int>(value);
129+
do {
130+
digits[count++] = static_cast<char>('0' + magnitude % 10);
131+
magnitude /= 10;
132+
} while (magnitude != 0 && count < sizeof(digits));
133+
while (count > 0 && length < capacity) {
134+
out[length++] = digits[--count];
135+
}
136+
}
137+
138+
void Handler(int signalNumber, siginfo_t* info, void* context) {
139+
// Only the first thread to fault records; the rest are already doomed.
140+
if (!g_recorded.test_and_set()) {
141+
int fd = g_storeFd;
142+
if (fd >= 0) {
143+
char header[kHeaderMax];
144+
size_t length = 0;
145+
AppendRaw(header, sizeof(header), length, "fatal signal ");
146+
AppendRawInt(header, sizeof(header), length, signalNumber);
147+
AppendRaw(header, sizeof(header), length, " on tid ");
148+
AppendRawInt(header, sizeof(header), length, CurrentTid());
149+
AppendRaw(header, sizeof(header), length, "\n");
150+
151+
ssize_t written = pwrite(fd, header, length, 0);
152+
int active = g_active.load(std::memory_order_acquire);
153+
if (written > 0 && active >= 0) {
154+
pwrite(fd, g_rendered[active], g_renderedLength[active], written);
155+
}
156+
}
157+
}
158+
159+
/*
160+
* Hand the signal to whoever owned it before us -- on Android that is
161+
* debuggerd, which writes the tombstone.
162+
*
163+
* A signal the kernel raised for a real fault arrives again on its own once
164+
* this returns and the faulting instruction re-executes, so debuggerd is
165+
* entered with the kernel's original siginfo instead of anything
166+
* synthesised here. One that was delivered by abort() or kill() (si_code
167+
* <= 0) will not come back, so it has to be re-raised explicitly.
168+
*/
169+
sigaction(signalNumber, &g_previous[signalNumber], nullptr);
170+
if (info == nullptr || info->si_code <= 0) {
171+
raise(signalNumber);
172+
}
173+
}
174+
175+
} // namespace
176+
177+
namespace tns {
178+
179+
void CrashBreadcrumbs::Install() {
180+
static std::once_flag once;
181+
std::call_once(once, [] {
182+
struct sigaction action = {};
183+
action.sa_sigaction = Handler;
184+
// SA_ONSTACK matters for a stack-overflow SIGSEGV, which has no room left
185+
// on the faulting stack to run a handler. bionic already gives every
186+
// thread an alternate signal stack, so the flag is all that is needed.
187+
action.sa_flags = SA_SIGINFO | SA_ONSTACK;
188+
sigemptyset(&action.sa_mask);
189+
for (int signalNumber : {SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE}) {
190+
sigaction(signalNumber, &action, &g_previous[signalNumber]);
191+
}
192+
});
193+
}
194+
195+
void CrashBreadcrumbs::OpenStore(const std::string& filesRoot) {
196+
static std::once_flag once;
197+
std::call_once(once, [&filesRoot] {
198+
std::string path = filesRoot + "/.ns-crash-breadcrumb";
199+
int fd = open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
200+
if (fd < 0) {
201+
return;
202+
}
203+
204+
char previous[kBufferMax + kHeaderMax];
205+
ssize_t length = read(fd, previous, sizeof(previous) - 1);
206+
if (length > 0) {
207+
previous[length] = '\0';
208+
// Deliberately not ANDROID_LOG_FATAL: liblog feeds a fatal record to
209+
// android_set_abort_message, and bionic keeps the first message it is
210+
// given for the life of the process. Claiming that slot here would
211+
// describe the *previous* process in this one's tombstone, and would
212+
// shut out the abort message libc or ART writes for the real fault.
213+
__android_log_print(
214+
ANDROID_LOG_ERROR, "TNS.Native",
215+
"The previous process was killed by a fatal signal. Runtime state "
216+
"recorded at that moment (match tid against the tombstone in "
217+
"/data/tombstones):\n%s",
218+
previous);
219+
ftruncate(fd, 0);
220+
}
221+
222+
g_storeFd = fd;
223+
});
224+
}
225+
226+
void CrashBreadcrumbs::RegisterRuntime(int runtimeId) {
227+
std::lock_guard<std::mutex> lock(g_mutex);
228+
Slot* slot = FindLocked(runtimeId);
229+
if (slot == nullptr) {
230+
for (Slot& candidate : g_slots) {
231+
if (!candidate.used) {
232+
slot = &candidate;
233+
break;
234+
}
235+
}
236+
}
237+
if (slot == nullptr) {
238+
// Table full. Keep the runtimes already tracked rather than evicting one.
239+
return;
240+
}
241+
242+
slot->used = true;
243+
slot->isWorker = false;
244+
slot->runtimeId = runtimeId;
245+
slot->tid = CurrentTid();
246+
slot->script[0] = '\0';
247+
slot->module[0] = '\0';
248+
t_slot = slot;
249+
RenderLocked();
250+
}
251+
252+
void CrashBreadcrumbs::UnregisterRuntime(int runtimeId) {
253+
std::lock_guard<std::mutex> lock(g_mutex);
254+
Slot* slot = FindLocked(runtimeId);
255+
if (slot == nullptr) {
256+
return;
257+
}
258+
if (t_slot == slot) {
259+
t_slot = nullptr;
260+
}
261+
slot->used = false;
262+
RenderLocked();
263+
}
264+
265+
void CrashBreadcrumbs::SetWorkerScript(int runtimeId, const char* script) {
266+
std::lock_guard<std::mutex> lock(g_mutex);
267+
Slot* slot = FindLocked(runtimeId);
268+
if (slot == nullptr) {
269+
return;
270+
}
271+
slot->isWorker = true;
272+
CopyField(slot->script, script);
273+
RenderLocked();
274+
}
275+
276+
void CrashBreadcrumbs::SetCurrentModule(const char* modulePath) {
277+
Slot* slot = t_slot;
278+
if (slot == nullptr) {
279+
return;
280+
}
281+
std::lock_guard<std::mutex> lock(g_mutex);
282+
CopyField(slot->module, modulePath);
283+
RenderLocked();
284+
}
285+
286+
} // namespace tns
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#ifndef CRASHBREADCRUMBS_H_
2+
#define CRASHBREADCRUMBS_H_
3+
4+
#include <string>
5+
6+
namespace tns {
7+
8+
/*
9+
* Records what each runtime thread was doing, so a process killed by a fatal
10+
* signal leaves behind more than a native backtrace.
11+
*
12+
* The state is rendered into a plain byte buffer as it changes, on ordinary
13+
* threads. At crash time the only work left is a write(2) of that buffer,
14+
* which is one of the few calls POSIX permits from a signal handler --
15+
* anything that allocates, takes a lock or formats has already happened.
16+
*/
17+
class CrashBreadcrumbs {
18+
public:
19+
/*
20+
* Installs SIGSEGV/SIGABRT/SIGBUS/SIGILL/SIGFPE handlers that record the
21+
* breadcrumb and then hand the signal back to the handler installed before
22+
* them, so debuggerd still writes the tombstone. Idempotent.
23+
*/
24+
static void Install();
25+
26+
/*
27+
* Points the store at the app's files directory and reports whatever a
28+
* previous process left behind. Idempotent, so every runtime may call it.
29+
*/
30+
static void OpenStore(const std::string& filesRoot);
31+
32+
/* Binds the calling thread to a runtime for that runtime's lifetime. */
33+
static void RegisterRuntime(int runtimeId);
34+
static void UnregisterRuntime(int runtimeId);
35+
36+
/* Marks a registered runtime as a worker started from `script`. */
37+
static void SetWorkerScript(int runtimeId, const char* script);
38+
39+
/* Records the module the calling runtime is about to execute. */
40+
static void SetCurrentModule(const char* modulePath);
41+
};
42+
43+
} // namespace tns
44+
45+
#endif /* CRASHBREADCRUMBS_H_ */

‎test-app/runtime/src/main/cpp/ModuleInternal.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "V8GlobalHelpers.h"
1414
#include "NativeScriptAssert.h"
1515
#include "Constants.h"
16+
#include "CrashBreadcrumbs.h"
1617
#include "NativeScriptException.h"
1718
#include "NsBuiltinModules.h"
1819
#include "napi/NapiModules.h"
@@ -363,6 +364,7 @@ Local<Object> ModuleInternal::LoadImpl(Isolate* isolate, const string& moduleNam
363364
Local<Object> ModuleInternal::LoadModule(Isolate* isolate, const string& modulePath, const string& moduleCacheKey) {
364365
string frameName("LoadModule " + modulePath);
365366
tns::instrumentation::Frame frame(frameName);
367+
CrashBreadcrumbs::SetCurrentModule(modulePath.c_str());
366368
Local<Object> result;
367369

368370
auto context = isolate->GetCurrentContext();

0 commit comments

Comments
 (0)