@cryptotaxi247 / netdata-1 / commits / cd741f258

Fix TOCTOU race in dictionary_destroy() (#22108)

* Fix TOCTOU race in dictionary_destroy() - Add synchronized checks to prevent accessing destroyed dictionaries in multithreaded scenarios. - Introduce unit tests to reproduce and validate the fix for concurrent get/set/traverse operations during dictionary destruction. - Update dictionary destruction logic to mark and lock the dictionary index, mitigating race conditions. * Improve TOCTOU test reliability in dictionary_destroy() - Add a timeout mechanism to prevent indefinite hangs during the test. - Implement child process termination to handle potential test deadlocks. * Fix race condition and improve error handling in dictionary traversal and unit tests - Ensure proper unlocking in `dictionary_foreach_done` to handle reentrant mode locks. - Add error handling in unit tests to clean up resources when thread creation fails. * Refactor TOCTOU test in `dictionary_destroy()` to improve process handling and readability - Replace `timed_out` flag with `reaped` for clarity. - Simplify logic for detecting and handling hung child processes. * Move CI environment detection to `libnetdata` and refactor calls - Implement `nd_is_running_under_ci()` in `libnetdata/os/ci.c` for centralized CI environment checking. - Replace the duplicate `is_ci()` logic in `status-file.c` with the new library function. - Update build files and headers to include the new module. * Fix dictionary destruction logic and enhance platform compatibility - Add `DICT_FLAG_QUEUED_FOR_DESTRUCTION` to track queued dictionaries. - Refactor `dictionary_queue_for_destruction()` to prevent redundant operations. - Improve `dictionary_destroy()` handling for destroyed or null dictionaries. - Add platform-specific handling for `dictionary_destroy_race_unittest`, skipping unsupported tests on Windows. * Clarify comments for safe repeated index teardown during dictionary destruction * Fix dictionary destruction handling in unit tests - Hold acquired item to ensure delayed destruction path is tested. - Add error handling for `dictionary_get_and_acquire_item` failures. - Ensure proper item release and dictionary cleanup in all cases. * Improve error handling and synchronization in dictionary destruction and CI detection - Fix EINTR handling in `dictionary_destroy()` TOCTOU tests for better reliability. - Add synchronization to `dictionary_queue_for_destruction` to prevent redundant locking. - Refactor CI environment detection to enhance modularity and include missing headers. * Use unique keys in dictionary destruction unit test * Refactor `dictionary_destroy_race_unittest` to simplify logic and improve synchronization - Remove permanent item acquisition to avoid forcing the old delayed-destroy path. - Rely on transient in-flight accesses to test synchronization during destruction. - Reduce iterations for CI to improve test reliability and runtime.

Stelios Fragkakis committed Apr 14, 2026 at 17:22 UTC cd741f258639abc01db8182be0e8268fd544640f
10 files changed +378 -48
CMakeLists.txt
+2
@@ -924,6 +924,8 @@ set(LIBNETDATA_FILES
924 src/libnetdata/log/nd_log.h
925 src/libnetdata/os/os.c
926 src/libnetdata/os/os.h
927 + src/libnetdata/os/ci.c
928 + src/libnetdata/os/ci.h
929 src/libnetdata/os/byteorder.h
930 src/libnetdata/onewayalloc/onewayalloc.c
931 src/libnetdata/onewayalloc/onewayalloc.h
src/daemon/status-file.c
+1 -42
@@ -1034,47 +1034,6 @@ struct log_priority PRI_FATAL = { NDLP_ERR, NDLP_ERR };
1034 struct log_priority PRI_DEADLY_SIGNAL = { NDLP_CRIT, NDLP_CRIT };
1035 struct log_priority PRI_KILLED_HARD = { NDLP_ERR, NDLP_WARNING };
1036
1037 -static bool is_ci(void) {
1038 - // List of known CI environment variables.
1039 - const char *ci_vars[] = {
1040 - "CI", // Generic CI flag
1041 - "CONTINUOUS_INTEGRATION", // Alternate generic flag
1042 - "BUILD_NUMBER", // Jenkins, TeamCity
1043 - "RUN_ID", // AWS CodeBuild, some others
1044 - "TRAVIS", // Travis CI
1045 - "GITHUB_ACTIONS", // GitHub Actions
1046 - "GITHUB_TOKEN", // GitHub Actions
1047 - "GITLAB_CI", // GitLab CI
1048 - "CIRCLECI", // CircleCI
1049 - "APPVEYOR", // AppVeyor
1050 - "BITBUCKET_BUILD_NUMBER", // Bitbucket Pipelines
1051 - "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", // Azure DevOps
1052 - "TF_BUILD", // Azure DevOps (alternate)
1053 - "BAMBOO_BUILDKEY", // Bamboo CI
1054 - "GO_PIPELINE_NAME", // GoCD
1055 - "HUDSON_URL", // Hudson CI
1056 - "TEAMCITY_VERSION", // TeamCity
1057 - "CI_NAME", // Some environments (e.g., CodeShip)
1058 - "CI_WORKER", // AppVeyor (alternate)
1059 - "CI_SERVER", // Generic
1060 - "HEROKU_TEST_RUN_ID", // Heroku CI
1061 - "BUILDKITE", // Buildkite
1062 - "DRONE", // Drone CI
1063 - "SEMAPHORE", // Semaphore CI
1064 - "NETLIFY", // Netlify CI
1065 - "NOW_BUILDER", // Vercel (formerly Zeit Now)
1066 - NULL
1067 - };
1068 -
1069 - // Iterate over the CI environment variable names.
1070 - for (const char **env = ci_vars; *env; env++) {
1071 - if(getenv(*env))
1072 - return true;
1073 - }
1074 -
1075 - return false;
1076 -}
1077 -
1037 enum crash_report_t {
1038 DSF_REPORT_DISABLED = 0,
1039 DSF_REPORT_ALL,
@@ -1353,7 +1312,7 @@ void daemon_status_file_check_crash(void) {
1312 (!no_previous_status || daemon_status_file_saved) &&
1313
1314 // we have more than 2 restarts, or this is not a CI run
1356 - (last_session_status.restarts > 1 || !is_ci()) &&
1315 + (last_session_status.restarts > 1 || !nd_is_running_under_ci()) &&
1316
1317 // we have not reported this
1318 !dedup_already_posted(&session_status, daemon_status_file_hash(&last_session_status, msg, cause), false)
src/libnetdata/dictionary/dictionary-internals.h
+1
@@ -10,6 +10,7 @@
10 typedef enum __attribute__ ((__packed__)) {
11 DICT_FLAG_NONE = 0,
12 DICT_FLAG_DESTROYED = (1 << 0), // this dictionary has been destroyed
13 + DICT_FLAG_QUEUED_FOR_DESTRUCTION = (1 << 1), // this dictionary is queued for delayed destruction
14 } DICT_FLAGS;
15
16 #define dict_flag_check(dict, flag) (__atomic_load_n(&((dict)->flags), __ATOMIC_RELAXED) & (flag))
src/libnetdata/dictionary/dictionary-item.h
+21
@@ -386,6 +386,11 @@ static inline bool dict_item_del(DICTIONARY *dict, const char *name, ssize_t nam
386
387 dictionary_index_lock_wrlock(dict);
388
389 + if(unlikely(is_dictionary_destroyed(dict))) {
390 + dictionary_index_wrlock_unlock(dict);
391 + return false;
392 + }
393 +
394 int ret;
395 DICTIONARY_ITEM *item = hashtable_get_unsafe(dict, name, name_len);
396 if(unlikely(!item)) {
@@ -440,6 +445,14 @@ static inline DICTIONARY_ITEM *dict_item_add_or_reset_value_and_acquire(DICTIONA
445
446 dictionary_index_lock_wrlock(dict);
447
448 + // Re-check under the index lock. This synchronizes with
449 + // dictionary_destroy(), which sets the destroyed flag and then takes
450 + // this lock before tearing down the index.
451 + if(unlikely(is_dictionary_destroyed(dict))) {
452 + dictionary_index_wrlock_unlock(dict);
453 + return NULL;
454 + }
455 +
456 bool added_or_updated = false;
457 size_t spins = 0;
458 DICTIONARY_ITEM *item = NULL;
@@ -541,6 +554,14 @@ static inline DICTIONARY_ITEM *dict_item_find_and_acquire(DICTIONARY *dict, cons
554
555 dictionary_index_lock_rdlock(dict);
556
557 + // Re-check under the index lock. This synchronizes with
558 + // dictionary_destroy(), which sets the destroyed flag and then takes
559 + // this lock before tearing down the index.
560 + if(unlikely(is_dictionary_destroyed(dict))) {
561 + dictionary_index_rdlock_unlock(dict);
562 + return NULL;
563 + }
564 +
565 DICTIONARY_ITEM *item = hashtable_get_unsafe(dict, name, name_len);
566 if(unlikely(item && !item_check_and_acquire(dict, item))) {
567 item = NULL;
src/libnetdata/dictionary/dictionary-traversal.c
+34 -1
@@ -25,6 +25,19 @@ void *dictionary_foreach_start_rw(DICTFE *dfe) {
25 dfe->locked = true;
26 ll_recursive_lock(dfe->dict, dfe->rw);
27
28 + // Re-check under the lock — dictionary_destroy() sets the flag while
29 + // holding this lock, so this is the synchronized check.
30 + if(unlikely(is_dictionary_destroyed(dfe->dict))) {
31 + ll_recursive_unlock(dfe->dict, dfe->rw);
32 + dfe->locked = false;
33 + dfe->dict = NULL;
34 + dfe->item = NULL;
35 + dfe->name = NULL;
36 + dfe->value = NULL;
37 + dfe->counter = 0;
38 + return NULL;
39 + }
40 +
41 // get the first item from the list
42 DICTIONARY_ITEM *item = dfe->dict->items.list;
43
@@ -63,6 +76,15 @@ ALWAYS_INLINE void *dictionary_foreach_next(DICTFE *dfe) {
76 if(unlikely(dfe->rw == DICTIONARY_LOCK_REENTRANT) || !dfe->locked) {
77 ll_recursive_lock(dfe->dict, dfe->rw);
78 dfe->locked = true;
79 +
80 + if(unlikely(is_dictionary_destroyed(dfe->dict))) {
81 + // Unlock before foreach_done — in reentrant mode, foreach_done
82 + // does not release the lock (it assumes the caller manages it).
83 + ll_recursive_unlock(dfe->dict, dfe->rw);
84 + dfe->locked = false;
85 + dictionary_foreach_done(dfe);
86 + return NULL;
87 + }
88 }
89
90 // the item we just did
@@ -147,6 +169,11 @@ int dictionary_walkthrough_rw(DICTIONARY *dict, char rw, dict_walkthrough_callba
169
170 ll_recursive_lock(dict, rw);
171
172 + if(unlikely(is_dictionary_destroyed(dict))) {
173 + ll_recursive_unlock(dict, rw);
174 + return 0;
175 + }
176 +
177 DICTIONARY_STATS_WALKTHROUGHS_PLUS1(dict);
178
179 // written in such a way, that the callback can delete the active element
@@ -208,9 +235,15 @@ int dictionary_sorted_walkthrough_rw(DICTIONARY *dict, char rw, dict_walkthrough
235 return 0;
236 }
237
238 + ll_recursive_lock(dict, rw);
239 +
240 + if(unlikely(is_dictionary_destroyed(dict))) {
241 + ll_recursive_unlock(dict, rw);
242 + return 0;
243 + }
244 +
245 DICTIONARY_STATS_WALKTHROUGHS_PLUS1(dict);
246
213 - ll_recursive_lock(dict, rw);
247 size_t entries = __atomic_load_n(&dict->entries, __ATOMIC_RELAXED);
248 DICTIONARY_ITEM **array = mallocz(sizeof(DICTIONARY_ITEM *) * entries);
249
src/libnetdata/dictionary/dictionary-unittest.c
+231
@@ -1009,6 +1009,235 @@ size_t dictionary_unittest_views(void) {
1009 return errors;
1010 }
1011
1012 +// ----------------------------------------------------------------------------
1013 +// Test: dictionary_destroy() TOCTOU race
1014 +//
1015 +// Stress test for the race where dictionary_destroy() could start force-freeing
1016 +// a dictionary while concurrent get/set/traversal operations were still able
1017 +// to enter through stale pre-lock destroyed-state checks.
1018 +//
1019 +// The test runs the racy workload in a forked child process so that a crash
1020 +// is detected as a child signal instead of bringing down the test harness.
1021 +
1022 +#ifndef OS_WINDOWS
1023 +#include <sys/wait.h>
1024 +#endif
1025 +
1026 +#ifndef OS_WINDOWS
1027 +
1028 +struct dict_destroy_race_data {
1029 + DICTIONARY *dict;
1030 + int ready; // atomic: worker signals it is looping
1031 + int stop; // atomic: main tells worker to stop
1032 +};
1033 +
1034 +// Worker that continuously acquires and releases an item.
1035 +// Keep the reference briefly so destroy() can observe an in-flight access in
1036 +// the post-index-teardown recheck without forcing the old "already referenced"
1037 +// path up front.
1038 +static void dict_destroy_race_getter_thread(void *arg) {
1039 + struct dict_destroy_race_data *d = arg;
1040 +
1041 + __atomic_store_n(&d->ready, 1, __ATOMIC_RELEASE);
1042 +
1043 + while(!__atomic_load_n(&d->stop, __ATOMIC_RELAXED)) {
1044 + DICTIONARY_ITEM *item = (DICTIONARY_ITEM *)dictionary_get_and_acquire_item(d->dict, "key");
1045 + if(item) {
1046 + const char *val = dictionary_acquired_item_value(item);
1047 + if(val) {
1048 + volatile char c __attribute__((unused)) = val[0];
1049 + }
1050 + tinysleep();
1051 + dictionary_acquired_item_release(d->dict, item);
1052 + }
1053 + }
1054 +}
1055 +
1056 +// Worker that continuously sets (inserts/updates) items.
1057 +static void dict_destroy_race_setter_thread(void *arg) {
1058 + struct dict_destroy_race_data *d = arg;
1059 +
1060 + __atomic_store_n(&d->ready, 1, __ATOMIC_RELEASE);
1061 +
1062 + int counter = 0;
1063 + while(!__atomic_load_n(&d->stop, __ATOMIC_RELAXED)) {
1064 + char key[32], val[32];
1065 + // Use unique keys so the test exercises concurrent inserts during
1066 + // destruction without racing on value replacement semantics.
1067 + snprintfz(key, sizeof(key), "key-%d", counter);
1068 + snprintfz(val, sizeof(val), "val-%d", counter);
1069 + dictionary_set(d->dict, key, val, strlen(val) + 1);
1070 + counter++;
1071 + }
1072 +}
1073 +
1074 +// Worker that continuously traverses (dfe_start_read / dfe_done).
1075 +static void dict_destroy_race_traverser_thread(void *arg) {
1076 + struct dict_destroy_race_data *d = arg;
1077 +
1078 + __atomic_store_n(&d->ready, 1, __ATOMIC_RELEASE);
1079 +
1080 + while(!__atomic_load_n(&d->stop, __ATOMIC_RELAXED)) {
1081 + void *val;
1082 + dfe_start_read(d->dict, val) {
1083 + if(val) {
1084 + volatile char c __attribute__((unused)) = ((const char *)val)[0];
1085 + }
1086 + }
1087 + dfe_done(val);
1088 + }
1089 +}
1090 +
1091 +// Run the racy workload in a child process: concurrent get/set/traverse
1092 +// while the main thread destroys the dictionary. Without the fix this may
1093 +// crash or trip internal consistency checks, depending on timing.
1094 +static void dict_destroy_race_child(int iterations) {
1095 + for(int i = 0; i < iterations; i++) {
1096 + DICTIONARY *dict = dictionary_create(DICT_OPTION_NONE);
1097 + dictionary_set(dict, "key", "value", 6);
1098 +
1099 + struct dict_destroy_race_data getter_data = { .dict = dict, .ready = 0, .stop = 0 };
1100 + struct dict_destroy_race_data setter_data = { .dict = dict, .ready = 0, .stop = 0 };
1101 + struct dict_destroy_race_data traverser_data = { .dict = dict, .ready = 0, .stop = 0 };
1102 +
1103 + ND_THREAD *getter = nd_thread_create(
1104 + "race-getter", NETDATA_THREAD_OPTION_DONT_LOG,
1105 + dict_destroy_race_getter_thread, &getter_data);
1106 +
1107 + ND_THREAD *setter = nd_thread_create(
1108 + "race-setter", NETDATA_THREAD_OPTION_DONT_LOG,
1109 + dict_destroy_race_setter_thread, &setter_data);
1110 +
1111 + ND_THREAD *traverser = nd_thread_create(
1112 + "race-trav", NETDATA_THREAD_OPTION_DONT_LOG,
1113 + dict_destroy_race_traverser_thread, &traverser_data);
1114 +
1115 + if(!getter || !setter || !traverser) {
1116 + // Thread creation failed — stop any that did start and clean up.
1117 + __atomic_store_n(&getter_data.stop, 1, __ATOMIC_RELEASE);
1118 + __atomic_store_n(&setter_data.stop, 1, __ATOMIC_RELEASE);
1119 + __atomic_store_n(&traverser_data.stop, 1, __ATOMIC_RELEASE);
1120 + if(getter) nd_thread_join(getter);
1121 + if(setter) nd_thread_join(setter);
1122 + if(traverser) nd_thread_join(traverser);
1123 + dictionary_destroy(dict);
1124 + cleanup_destroyed_dictionaries(false);
1125 + _exit(2);
1126 + }
1127 +
1128 + // wait for all workers to be running
1129 + while(!__atomic_load_n(&getter_data.ready, __ATOMIC_ACQUIRE) ||
1130 + !__atomic_load_n(&setter_data.ready, __ATOMIC_ACQUIRE) ||
1131 + !__atomic_load_n(&traverser_data.ready, __ATOMIC_ACQUIRE))
1132 + tinysleep();
1133 +
1134 + tinysleep();
1135 +
1136 + // Do not hold a permanent acquired item here: that would force the old
1137 + // "already referenced" delayed-destroy path before destroy() reaches
1138 + // the new destroyed-flag + index-teardown synchronization. Instead,
1139 + // rely on the active workers to create transient in-flight accesses
1140 + // while destroy() races with get/set/traversal.
1141 + dictionary_destroy(dict);
1142 +
1143 + __atomic_store_n(&getter_data.stop, 1, __ATOMIC_RELEASE);
1144 + __atomic_store_n(&setter_data.stop, 1, __ATOMIC_RELEASE);
1145 + __atomic_store_n(&traverser_data.stop, 1, __ATOMIC_RELEASE);
1146 + nd_thread_join(getter);
1147 + nd_thread_join(setter);
1148 + nd_thread_join(traverser);
1149 +
1150 + cleanup_destroyed_dictionaries(false);
1151 + }
1152 +}
1153 +
1154 +static int dictionary_destroy_race_unittest(void) {
1155 + const int iterations = nd_is_running_under_ci() ? 10 : 100;
1156 +
1157 + fprintf(stderr,
1158 + "\nTesting dictionary_destroy() TOCTOU race (%d iterations in child process)...\n",
1159 + iterations);
1160 +
1161 + fflush(stderr);
1162 + fflush(stdout);
1163 +
1164 + pid_t pid = fork();
1165 + if(pid == 0) {
1166 + // child — run the racy workload
1167 + dict_destroy_race_child(iterations);
1168 + _exit(0);
1169 + }
1170 +
1171 + if(pid < 0) {
1172 + fprintf(stderr, "dictionary_destroy() TOCTOU race test: fork() failed: %s\n",
1173 + strerror(errno));
1174 + return 1;
1175 + }
1176 +
1177 + // Give the child a generous timeout so a hang doesn't stall the suite.
1178 + int timeout_sec = 120;
1179 + int status = 0;
1180 + bool reaped = false;
1181 + for(int elapsed = 0; elapsed < timeout_sec; elapsed++) {
1182 + pid_t rc = waitpid(pid, &status, WNOHANG);
1183 + if(rc > 0) { reaped = true; break; }
1184 + if(rc < 0) {
1185 + if(errno == EINTR)
1186 + continue;
1187 + fprintf(stderr, "dictionary_destroy() TOCTOU race test: waitpid() failed: %s\n",
1188 + strerror(errno));
1189 + return 1;
1190 + }
1191 + sleep_usec(USEC_PER_SEC);
1192 + }
1193 + if(!reaped) {
1194 + kill(pid, SIGKILL);
1195 + while(waitpid(pid, &status, 0) < 0) {
1196 + if(errno != EINTR) {
1197 + fprintf(stderr, "dictionary_destroy() TOCTOU race test: waitpid() failed after SIGKILL: %s\n",
1198 + strerror(errno));
1199 + return 1;
1200 + }
1201 + }
1202 + fprintf(stderr, "dictionary_destroy() TOCTOU race test: FAILED — "
1203 + "child hung (killed after %d seconds)\n", timeout_sec);
1204 + return 1;
1205 + }
1206 +
1207 + if(WIFSIGNALED(status)) {
1208 + int sig = WTERMSIG(status);
1209 + fprintf(stderr,
1210 + "dictionary_destroy() TOCTOU race test: FAILED — "
1211 + "child killed by signal %d (%s) — "
1212 + "dictionary_destroy() still has a TOCTOU in its destroy/access "
1213 + "synchronization path\n",
1214 + sig, strsignal(sig));
1215 + return 1;
1216 + }
1217 +
1218 + if(WIFEXITED(status) && WEXITSTATUS(status) != 0) {
1219 + fprintf(stderr,
1220 + "dictionary_destroy() TOCTOU race test: FAILED — "
1221 + "child exited with status %d\n",
1222 + WEXITSTATUS(status));
1223 + return 1;
1224 + }
1225 +
1226 + fprintf(stderr, "dictionary_destroy() TOCTOU race test: OK\n");
1227 + return 0;
1228 +}
1229 +
1230 +#else
1231 +
1232 +static int dictionary_destroy_race_unittest(void) {
1233 + fprintf(stderr,
1234 + "\nTesting dictionary_destroy() TOCTOU race: SKIPPED "
1235 + "(fork-based test is unsupported on this platform)\n");
1236 + return 0;
1237 +}
1238 +
1239 +#endif
1240 +
1241 bool dictionary_traverse_or_destroy_unittest(void) {
1242 DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED);
1243 dictionary_set(dict, "KEY 1", "VALUE1", strlen("VALUE1") + 1);
@@ -1635,6 +1864,8 @@ int dictionary_unittest(size_t entries) {
1864 else
1865 fprintf(stderr, "Destroy on traversal test OK\n");
1866
1867 + errors += dictionary_destroy_race_unittest();
1868 +
1869 cleanup_destroyed_dictionaries(false);
1870
1871 size_t delayed = dictionary_destroy_delayed_count();
src/libnetdata/dictionary/dictionary.c
+33 -5
@@ -327,17 +327,19 @@ DEFINE_JUDYL_TYPED(STACKTRACE, size_t);
327 #endif
328
329 static void dictionary_queue_for_destruction(DICTIONARY *dict) {
330 - if(is_dictionary_destroyed(dict))
331 - return;
330 + netdata_mutex_lock(&dictionaries_waiting_to_be_destroyed_mutex);
331 +
332 + if(dict_flag_check(dict, DICT_FLAG_QUEUED_FOR_DESTRUCTION))
333 + goto cleanup;
334
335 DICTIONARY_STATS_DICT_DESTROY_QUEUED_PLUS1(dict);
336 dict_flag_set(dict, DICT_FLAG_DESTROYED);
335 -
336 - netdata_mutex_lock(&dictionaries_waiting_to_be_destroyed_mutex);
337 + dict_flag_set(dict, DICT_FLAG_QUEUED_FOR_DESTRUCTION);
338
339 dict->next = dictionaries_waiting_to_be_destroyed;
340 dictionaries_waiting_to_be_destroyed = dict;
341
342 +cleanup:
343 netdata_mutex_unlock(&dictionaries_waiting_to_be_destroyed_mutex);
344 }
345
@@ -661,7 +663,8 @@ void dictionary_flush(DICTIONARY *dict) {
663 size_t dictionary_destroy(DICTIONARY *dict) {
664 cleanup_destroyed_dictionaries(false);
665
664 - if(!dict) return 0;
666 + if(!dict || unlikely(is_dictionary_destroyed(dict)))
667 + return 0;
668
669 ll_recursive_lock(dict, DICTIONARY_LOCK_WRITE);
670
@@ -681,6 +684,31 @@ size_t dictionary_destroy(DICTIONARY *dict) {
684 return 0;
685 }
686
687 + dict_flag_set(dict, DICT_FLAG_DESTROYED);
688 +
689 + // Destroy the index while holding the items write lock.
690 + // This prevents a TOCTOU race: without this, a reader could pass the
691 + // is_dictionary_destroyed() check, then acquire an item via the index,
692 + // after we've decided to force-free all items.
693 + // By destroying the index here, any concurrent dictionary_get_and_acquire_item()
694 + // that acquires the index lock after this point will find an empty index.
695 + // This uses hashtable_destroy_unsafe(); the later cleanup path in
696 + // dictionary_free_all_resources() may invoke the same index teardown
697 + // again, so this relies on that full destruction flow being safe to repeat.
698 + dictionary_index_lock_wrlock(dict);
699 + hashtable_destroy_unsafe(dict);
700 + dictionary_index_wrlock_unlock(dict);
701 +
702 + // Re-check: a reader that held the index read lock during the destroy
703 + // above may have acquired an item before we got the index write lock.
704 + // If so, fall back to the deferred destruction path.
705 + if(dictionary_referenced_items(dict)) {
706 + dictionary_queue_for_destruction(dict);
707 +
708 + ll_recursive_unlock(dict, DICTIONARY_LOCK_WRITE);
709 + return 0;
710 + }
711 +
712 ll_recursive_unlock(dict, DICTIONARY_LOCK_WRITE);
713
714 size_t freed;
src/libnetdata/libnetdata.h
+1
@@ -88,6 +88,7 @@ extern const char *netdata_configured_host_prefix;
88
89 // this may include windows.h
90 #include "os/os.h"
91 +#include "os/ci.h"
92
93 #include "socket/socket.h"
94 #include "socket/nd-sock.h"
src/libnetdata/os/ci.c new
+44
@@ -0,0 +1,44 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "ci.h"
4 +
5 +#include <stdlib.h>
6 +
7 +bool nd_is_running_under_ci(void) {
8 + const char *ci_vars[] = {
9 + "CI", // Generic CI flag
10 + "CONTINUOUS_INTEGRATION", // Alternate generic flag
11 + "BUILD_NUMBER", // Jenkins, TeamCity
12 + "RUN_ID", // AWS CodeBuild, some others
13 + "TRAVIS", // Travis CI
14 + "GITHUB_ACTIONS", // GitHub Actions
15 + "GITHUB_TOKEN", // GitHub Actions
16 + "GITLAB_CI", // GitLab CI
17 + "CIRCLECI", // CircleCI
18 + "APPVEYOR", // AppVeyor
19 + "BITBUCKET_BUILD_NUMBER", // Bitbucket Pipelines
20 + "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", // Azure DevOps
21 + "TF_BUILD", // Azure DevOps (alternate)
22 + "BAMBOO_BUILDKEY", // Bamboo CI
23 + "GO_PIPELINE_NAME", // GoCD
24 + "HUDSON_URL", // Hudson CI
25 + "TEAMCITY_VERSION", // TeamCity
26 + "CI_NAME", // Some environments (e.g., CodeShip)
27 + "CI_WORKER", // AppVeyor (alternate)
28 + "CI_SERVER", // Generic
29 + "HEROKU_TEST_RUN_ID", // Heroku CI
30 + "BUILDKITE", // Buildkite
31 + "DRONE", // Drone CI
32 + "SEMAPHORE", // Semaphore CI
33 + "NETLIFY", // Netlify CI
34 + "NOW_BUILDER", // Vercel (formerly Zeit Now)
35 + NULL
36 + };
37 +
38 + for (const char **env = ci_vars; *env; env++) {
39 + if(getenv(*env))
40 + return true;
41 + }
42 +
43 + return false;
44 +}
src/libnetdata/os/ci.h new
+10
@@ -0,0 +1,10 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_OS_CI_H
4 +#define NETDATA_OS_CI_H
5 +
6 +#include <stdbool.h>
7 +
8 +bool nd_is_running_under_ci(void);
9 +
10 +#endif