Capture deadly signals (#19737)
* capture deadly signals on all threads; add the thread name to daemon-status-file * do not overwrite the thread * sort the exit reasons so that deadly signals are first * add -rdynamic when appropriate * log stack traces only on warnings and errors * increase status file version to 6
Costa Tsaousis committed
Mar 1, 2025 at 12:52 UTC
abd80bee16fdf41b6258b6726892aace20a3fff2
9 files changed
+150
-86
CMakeLists.txt
+9
-1
@@ -486,6 +486,14 @@ check_function_exists(sysinfo HAVE_SYSINFO)
486
487
check_function_exists(timegm HAVE_TIMEGM)
488
489
+# Add -rdynamic when backtrace is available but libunwind is not
490
+if(HAVE_BACKTRACE AND NOT ENABLE_LIBUNWIND AND NOT STATIC_BUILD)
491
+ if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
492
+ add_link_options(-rdynamic)
493
+ message(STATUS "Adding -rdynamic to link options for better backtrace support")
494
+ endif()
495
+endif()
496
+
497
#
498
# check source compilation
499
#
@@ -1030,7 +1038,7 @@ set(LIBNETDATA_FILES
1038
src/libnetdata/locks/benchmark.h
1039
src/libnetdata/locks/benchmark-rw.c
1040
src/libnetdata/locks/benchmark-rw.h
1033
- src/libnetdata/log/nd_log-libunwind.c
1041
+ src/libnetdata/log/nd_log-stacktrace.c
1042
src/libnetdata/memory/nd-mallocz.c
1043
src/libnetdata/memory/nd-mallocz.h
1044
src/libnetdata/memory/nd-mmap.c
src/daemon/daemon-status-file.c
+43
-15
@@ -9,7 +9,7 @@
9
#include <openssl/pem.h>
10
#include <openssl/err.h>
11
12
-#define STATUS_FILE_VERSION 5
12
+#define STATUS_FILE_VERSION 6
13
14
#define STATUS_FILENAME "status-netdata.json"
15
@@ -152,6 +152,7 @@ static void daemon_status_file_to_json(BUFFER *wb, DAEMON_STATUS_FILE *ds) {
152
buffer_json_member_add_string_or_empty(wb, "function", ds->fatal.function);
153
buffer_json_member_add_string_or_empty(wb, "message", ds->fatal.message);
154
buffer_json_member_add_string_or_empty(wb, "errno", ds->fatal.errno_str);
155
+ buffer_json_member_add_string_or_empty(wb, "thread", ds->fatal.thread);
156
buffer_json_member_add_string_or_empty(wb, "stack_trace", ds->fatal.stack_trace);
157
}
158
buffer_json_object_close(wb);
@@ -190,6 +191,7 @@ static bool daemon_status_file_from_json(json_object *jobj, void *data, BUFFER *
191
bool required_v1 = version >= 1 ? strict : false;
192
bool required_v3 = version >= 3 ? strict : false;
193
bool required_v4 = version >= 4 ? strict : false;
194
+ bool required_v5 = version >= 5 ? strict : false;
195
196
// Parse timestamp
197
JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "@timestamp", datetime, error, required_v1);
@@ -264,9 +266,10 @@ static bool daemon_status_file_from_json(json_object *jobj, void *data, BUFFER *
266
JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "filename", ds->fatal.filename, error, required_v1);
267
JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "function", ds->fatal.function, error, required_v1);
268
JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "message", ds->fatal.message, error, required_v1);
267
- JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "errno", ds->fatal.errno_str, error, required_v3);
268
- JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "stack_trace", ds->fatal.stack_trace, error, required_v1);
269
+ JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "stack_trace", ds->fatal.stack_trace, error, required_v1);
270
JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, path, "line", ds->fatal.line, error, required_v1);
271
+ JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "errno", ds->fatal.errno_str, error, required_v3);
272
+ JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "thread", ds->fatal.thread, error, required_v5);
273
});
274
275
// Parse the last posted object
@@ -536,20 +539,25 @@ static bool save_status_file(const char *directory, const char *content, size_t
539
return true;
540
}
541
542
+static BUFFER *static_save_buffer = NULL;
543
+static void static_save_buffer_init(void) {
544
+ if (!static_save_buffer)
545
+ static_save_buffer = buffer_create(16384, NULL);
546
+
547
+ buffer_flush(static_save_buffer);
548
+}
549
+
550
static void daemon_status_file_save(DAEMON_STATUS_FILE *ds) {
551
spinlock_lock(&dsf_spinlock);
552
542
- static BUFFER *wb = NULL;
543
- if (!wb)
544
- wb = buffer_create(16384, NULL);
553
+ static_save_buffer_init();
554
546
- buffer_flush(wb);
547
- buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT);
548
- daemon_status_file_to_json(wb, ds);
549
- buffer_json_finalize(wb);
555
+ buffer_json_initialize(static_save_buffer, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT);
556
+ daemon_status_file_to_json(static_save_buffer, ds);
557
+ buffer_json_finalize(static_save_buffer);
558
551
- const char *content = buffer_tostring(wb);
552
- size_t content_size = buffer_strlen(wb);
559
+ const char *content = buffer_tostring(static_save_buffer);
560
+ size_t content_size = buffer_strlen(static_save_buffer);
561
562
// Try primary directory first
563
bool saved = false;
@@ -591,6 +599,23 @@ static void daemon_status_file_out_of_memory(void) {
599
daemon_status_file_exit_reason_save(EXIT_REASON_OUT_OF_MEMORY);
600
}
601
602
+void daemon_status_file_bad_signal_received(EXIT_REASON reason) {
603
+ spinlock_lock(&dsf_spinlock);
604
+ session_status.exit_reason |= reason;
605
+
606
+ if(!session_status.fatal.thread[0])
607
+ strncpyz(session_status.fatal.thread, nd_thread_tag(), sizeof(session_status.fatal.thread) - 1);
608
+
609
+ if(!session_status.fatal.stack_trace[0]) {
610
+ static_save_buffer_init();
611
+ capture_stack_trace(static_save_buffer);
612
+ strncpyz(session_status.fatal.stack_trace, buffer_tostring(static_save_buffer), sizeof(session_status.fatal.stack_trace) - 1);
613
+ }
614
+
615
+ spinlock_unlock(&dsf_spinlock);
616
+ daemon_status_file_save(&session_status);
617
+}
618
+
619
// --------------------------------------------------------------------------------------------------------------------
620
// deduplication hashes management
621
@@ -948,7 +973,7 @@ void daemon_status_file_register_fatal(const char *filename, const char *functio
973
spinlock_lock(&dsf_spinlock);
974
975
// do not check the function, because it may have a startup step in it
951
- if(session_status.fatal.filename || session_status.fatal.message || session_status.fatal.errno_str || session_status.fatal.stack_trace) {
976
+ if(session_status.fatal.filename || session_status.fatal.message || session_status.fatal.errno_str || session_status.fatal.thread[0]) {
977
spinlock_unlock(&dsf_spinlock);
978
freez((void *)filename);
979
freez((void *)function);
@@ -958,16 +983,19 @@ void daemon_status_file_register_fatal(const char *filename, const char *functio
983
return;
984
}
985
986
+ exit_initiated |= EXIT_REASON_FATAL;
987
+ strncpyz(session_status.fatal.thread, nd_thread_tag(), sizeof(session_status.fatal.thread) - 1);
988
+
989
session_status.fatal.filename = filename;
990
freez((char *)session_status.fatal.function); // it may have a startup step
991
session_status.fatal.function = function;
992
session_status.fatal.message = message;
993
session_status.fatal.errno_str = errno_str;
966
- session_status.fatal.stack_trace = stack_trace;
994
+ strncpyz(session_status.fatal.stack_trace, stack_trace, sizeof(session_status.fatal.stack_trace) - 1);
995
+ freez((char *)stack_trace);
996
session_status.fatal.line = line;
997
998
spinlock_unlock(&dsf_spinlock);
999
971
- exit_initiated |= EXIT_REASON_FATAL;
1000
daemon_status_file_save(&session_status);
1001
}
src/daemon/daemon-status-file.h
+3
-1
@@ -66,8 +66,9 @@ typedef struct daemon_status_file {
66
const char *filename;
67
const char *function;
68
const char *errno_str;
69
- const char *stack_trace;
69
const char *message;
70
+ char stack_trace[4096];
71
+ char thread[ND_THREAD_TAG_MAX + 1];
72
} fatal;
73
74
struct {
@@ -82,6 +83,7 @@ DAEMON_STATUS_FILE daemon_status_file_load(void);
83
// saves the current status
84
void daemon_status_file_update_status(DAEMON_STATUS status);
85
void daemon_status_file_exit_reason_save(EXIT_REASON reason);
86
+void daemon_status_file_bad_signal_received(EXIT_REASON reason);
87
88
// check for a crash
89
void daemon_status_file_check_crash(void);
src/daemon/signals.c
+18
-7
@@ -42,18 +42,22 @@ static void signal_handler(int signo) {
42
}
43
44
int i;
45
- for(i = 0; signals_waiting[i].action != NETDATA_SIGNAL_END_OF_LIST ; i++) {
45
+ for(i = 0; signals_waiting[i].action != NETDATA_SIGNAL_END_OF_LIST; i++) {
46
if(unlikely(signals_waiting[i].signo == signo)) {
47
signals_waiting[i].count++;
48
49
if(signals_waiting[i].action == NETDATA_SIGNAL_FATAL) {
50
char buffer[200 + 1];
51
- snprintfz(buffer, sizeof(buffer) - 1, "\nSIGNAL HANDLER: received: %s. Oops! This is bad!\n", signals_waiting[i].name);
51
+ snprintfz(buffer, sizeof(buffer) - 1, "\nSIGNAL HANDLER: received: %s in thread %d. Oops! This is bad!\n",
52
+ signals_waiting[i].name, gettid_cached());
53
+
54
if(write(STDERR_FILENO, buffer, strlen(buffer)) == -1) {
55
// nothing to do - we cannot write but there is no way to complain about it
56
;
57
}
56
- daemon_status_file_exit_reason_save(signals_waiting[i].reason);
58
+
59
+ // Always update the status file for fatal signals
60
+ daemon_status_file_bad_signal_received(signals_waiting[i].reason);
61
}
62
63
break;
@@ -63,15 +67,22 @@ static void signal_handler(int signo) {
67
__atomic_sub_fetch(&recurse, 1, __ATOMIC_RELAXED);
68
}
69
70
+
71
// Mask all signals, to ensure they will only be unmasked at the threads that can handle them.
72
// This means that all third party libraries (including libuv) cannot use signals anymore.
73
// The signals they are interested must be unblocked at their corresponding event loops.
69
-static void posix_mask_all_signals(void) {
74
+static void posix_signals_default_mask(void) {
75
sigset_t sigset;
76
sigfillset(&sigset);
77
78
+ // Don't mask fatal signals - we want these to be handled in any thread
79
+ sigdelset(&sigset, SIGBUS);
80
+ sigdelset(&sigset, SIGSEGV);
81
+ sigdelset(&sigset, SIGFPE);
82
+ sigdelset(&sigset, SIGILL);
83
+
84
if(pthread_sigmask(SIG_BLOCK, &sigset, NULL) != 0)
74
- netdata_log_error("SIGNAL: cannot mask all signals");
85
+ netdata_log_error("SIGNAL: cannot apply the default mask for signals");
86
}
87
88
// Unmask all signals the netdata main signal handler uses.
@@ -88,7 +99,7 @@ static void posix_unmask_my_signals(void) {
99
}
100
101
void nd_initialize_signals(void) {
91
- posix_mask_all_signals(); // block all signals for all threads
102
+ posix_signals_default_mask();
103
104
// Catch signals which we want to use
105
struct sigaction sa;
@@ -164,7 +175,7 @@ void nd_process_signals(void) {
175
176
case NETDATA_SIGNAL_FATAL:
177
nd_log_limits_unlimited();
167
- daemon_status_file_exit_reason_save(signals_waiting[i].reason);
178
+ daemon_status_file_bad_signal_received(signals_waiting[i].reason);
179
fatal("SIGNAL: Received %s. netdata now exits.", name);
180
break;
181
src/libnetdata/exit/exit_initiated.h
+19
-19
@@ -9,31 +9,31 @@
9
typedef enum {
10
EXIT_REASON_NONE = 0,
11
12
- // automatically detect when exit_initiated_set() is called
13
- // supports Linux, FreeBSD, MacOS, Windows
14
- EXIT_REASON_SYSTEM_SHUTDOWN = (1 << 0), // detected
15
-
16
- // signals - normal termination
17
- EXIT_REASON_SIGQUIT = (1 << 1), // rare, but graceful
18
- EXIT_REASON_SIGTERM = (1 << 2), // received on Linux, FreeBSD, MacOS
19
- EXIT_REASON_SIGINT = (1 << 3), // received on Windows on normal termination
20
-
12
// signals - abnormal termination
22
- EXIT_REASON_SIGBUS = (1 << 4),
23
- EXIT_REASON_SIGSEGV = (1 << 5),
24
- EXIT_REASON_SIGFPE = (1 << 6),
25
- EXIT_REASON_SIGILL = (1 << 7),
26
- EXIT_REASON_OUT_OF_MEMORY = (1 << 8),
13
+ EXIT_REASON_SIGBUS = (1 << 0),
14
+ EXIT_REASON_SIGSEGV = (1 << 1),
15
+ EXIT_REASON_SIGFPE = (1 << 2),
16
+ EXIT_REASON_SIGILL = (1 << 3),
17
+ EXIT_REASON_OUT_OF_MEMORY = (1 << 4),
18
+
19
+ // abnormal termination via a fatal message
20
+ EXIT_REASON_FATAL = (1 << 5), // a fatal message
21
22
// normal termination via APIs
29
- EXIT_REASON_API_QUIT = (1 << 9),
30
- EXIT_REASON_CMD_EXIT = (1 << 10),
23
+ EXIT_REASON_API_QUIT = (1 << 6), // developer only
24
+ EXIT_REASON_CMD_EXIT = (1 << 7), // netdatacli
25
32
- // abnormal termination via a fatal message
33
- EXIT_REASON_FATAL = (1 << 11),
26
+ // signals - normal termination
27
+ EXIT_REASON_SIGQUIT = (1 << 8), // rare, but graceful
28
+ EXIT_REASON_SIGTERM = (1 << 9), // received on Linux, FreeBSD, MacOS
29
+ EXIT_REASON_SIGINT = (1 << 10), // received on Windows on normal termination
30
31
// windows specific, service stop
36
- EXIT_REASON_SERVICE_STOP = (1 << 12),
32
+ EXIT_REASON_SERVICE_STOP = (1 << 11),
33
+
34
+ // automatically detect when exit_initiated_set() is called
35
+ // supports Linux, FreeBSD, MacOS, Windows
36
+ EXIT_REASON_SYSTEM_SHUTDOWN = (1 << 12),
37
38
// netdata update
39
EXIT_REASON_UPDATE = (1 << 13),
src/libnetdata/log/nd_log-init.c
+2
@@ -281,6 +281,8 @@ int nd_log_systemd_journal_fd(void) {
281
282
void nd_log_reopen_log_files_for_spawn_server(const char *name) {
283
nd_log_forked = true;
284
+ nd_log.log_event_cb = NULL;
285
+ nd_log.fatal_event_cb = NULL;
286
287
gettid_uncached();
288
src/libnetdata/log/nd_log-stacktrace.c
renamed
+54
-29
@@ -4,26 +4,10 @@
4
5
bool nd_log_forked = false;
6
7
-#ifdef HAVE_LIBUNWIND
7
+#if defined(HAVE_LIBUNWIND)
8
#include <libunwind.h>
9
10
-bool stack_trace_formatter(BUFFER *wb, void *data __maybe_unused) {
11
- static __thread bool in_stack_trace = false;
12
-
13
- if (nd_log_forked) {
14
- // libunwind freezes in forked children
15
- buffer_strcat(wb, "stack trace after fork is disabled");
16
- return true;
17
- }
18
-
19
- if (in_stack_trace) {
20
- // Prevent recursion
21
- buffer_strcat(wb, "stack trace recursion detected");
22
- return true;
23
- }
24
-
25
- in_stack_trace = true;
26
-
10
+void capture_stack_trace(BUFFER *wb) {
11
unw_cursor_t cursor;
12
unw_context_t context;
13
size_t frames = 0;
@@ -32,11 +16,11 @@ bool stack_trace_formatter(BUFFER *wb, void *data __maybe_unused) {
16
unw_getcontext(&context);
17
unw_init_local(&cursor, &context);
18
35
-// // Skip first 3 frames (our logging infrastructure)
36
-// for (int i = 0; i < 3; i++) {
37
-// if (unw_step(&cursor) <= 0)
38
-// goto cleanup; // Ensure proper cleanup if unwinding fails early
39
-// }
19
+ // // Skip first 3 frames (our logging infrastructure)
20
+ // for (int i = 0; i < 3; i++) {
21
+ // if (unw_step(&cursor) <= 0)
22
+ // goto cleanup; // Ensure proper cleanup if unwinding fails early
23
+ // }
24
25
while (unw_step(&cursor) > 0) {
26
unw_word_t offset, pc;
@@ -57,16 +41,57 @@ bool stack_trace_formatter(BUFFER *wb, void *data __maybe_unused) {
41
buffer_strcat(wb, "<unknown>");
42
}
43
}
44
+}
45
61
- in_stack_trace = false; // Ensure the flag is reset
62
- return true;
46
+#elif defined(HAVE_BACKTRACE)
47
+
48
+void capture_stack_trace(BUFFER *wb) {
49
+ void *array[50];
50
+ char **messages;
51
+ int size, i;
52
+
53
+ size = backtrace(array, _countof(array));
54
+ messages = backtrace_symbols(array, size);
55
+
56
+ if (messages == NULL) {
57
+ // Failed to get backtrace symbols
58
+ return;
59
+ }
60
+
61
+ // Format the stack trace
62
+ for (i = 0; i < size ; i++)
63
+ buffer_sprintf(wb, "#%d %s\n", i, messages[i]);
64
+
65
+ free(messages);
66
}
67
65
-#else // !HAVE_LIBUNWIND
68
+#else
69
67
-bool stack_trace_formatter(BUFFER *wb, void *data __maybe_unused) {
68
- buffer_strcat(wb, "libunwind not available");
69
- return true;
70
+void capture_stack_trace(BUFFER *wb) {
71
+ buffer_strcat(wb, "stack trace not available");
72
}
73
74
#endif
75
+
76
+bool stack_trace_formatter(BUFFER *wb, void *data __maybe_unused) {
77
+ static __thread bool in_stack_trace = false;
78
+
79
+ if (nd_log_forked) {
80
+ // libunwind freezes in forked children
81
+ buffer_strcat(wb, "stack trace after fork is disabled");
82
+ return true;
83
+ }
84
+
85
+ if (in_stack_trace) {
86
+ // Prevent recursion
87
+ buffer_strcat(wb, "stack trace recursion detected");
88
+ return true;
89
+ }
90
+
91
+ in_stack_trace = true;
92
+
93
+ capture_stack_trace(wb);
94
+
95
+ in_stack_trace = false; // Ensure the flag is reset
96
+ return true;
97
+}
src/libnetdata/log/nd_log.c
+1
-14
@@ -268,7 +268,7 @@ static void nd_logger(const char *file, const char *function, const unsigned lon
268
269
// set the common fields that are automatically set by the logging subsystem
270
271
- if(likely(!thread_log_fields[NDF_STACK_TRACE].entry.set))
271
+ if(likely(!thread_log_fields[NDF_STACK_TRACE].entry.set) && priority <= NDLP_WARNING)
272
thread_log_fields[NDF_STACK_TRACE].entry = ND_LOG_FIELD_CB(NDF_STACK_TRACE, stack_trace_formatter, NULL);
273
274
if(likely(!thread_log_fields[NDF_INVOCATION_ID].entry.set))
@@ -517,19 +517,6 @@ void netdata_logger_fatal(const char *file, const char *function, const unsigned
517
char action_result[200+1];
518
snprintfz(action_result, 60, "%s:%s:%s", program_name, tag_to_send, function);
519
520
-#if !defined(ENABLE_SENTRY) && defined(HAVE_BACKTRACE)
521
- int fd = nd_log.sources[NDLS_DAEMON].fd;
522
- if(fd == -1)
523
- fd = STDERR_FILENO;
524
-
525
- int nptrs;
526
- void *buffer[10000];
527
-
528
- nptrs = backtrace(buffer, sizeof(buffer));
529
- if(nptrs)
530
- backtrace_symbols_fd(buffer, nptrs, fd);
531
-#endif
532
-
520
#ifdef NETDATA_INTERNAL_CHECKS
521
// abort();
522
#endif
src/libnetdata/log/nd_log.h
+1
@@ -32,6 +32,7 @@ int nd_log_priority2id(const char *priority);
32
const char *nd_log_id2priority(ND_LOG_FIELD_PRIORITY priority);
33
const char *nd_log_method_for_external_plugins(const char *s);
34
ND_UUID nd_log_get_invocation_id(void);
35
+void capture_stack_trace(BUFFER *wb);
36
37
typedef void (*log_event_t)(const char *filename, const char *function, const char *message, const char *errno_str, const char *stack_trace, long line);
38
void nd_log_register_event_cb(log_event_t cb);