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);
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();