@cryptotaxi247 / netdata-1 / commits / eee9c5527

Fix memory-safety and correctness bugs surfaced by Coverity audit (part 3) (#22268)

* xenstat: skip domains without libxl domain info Coverity CID 413854 (UNINIT): skip the current domain when `libxl_domain_info()` fails before hashing or storing its UUID. This avoids using an uninitialized `uuid` buffer on the error path in `xenstat_collect()`. * datetime: fix rfc3339 fractional scaling Coverity CID 457729 (INTEGER_OVERFLOW): print_fraction() underflowed its loop bound when callers requested 7-9 fractional digits. Scale microseconds down for shorter output and pad zeros for 7-9 digits so the formatter preserves the existing 1..9-digit contract without hanging. * libnetdata: grow cpuset cpu parser buffer Coverity CID 425864 (TAINTED_SCALAR): os_read_cpuset_cpus() sized its static buffer from the first caller's system_cpus argument, and startup can first call it with 0. Derive a non-zero CPU baseline when needed and grow the buffer before reading cpuset.cpus so long CPU lists are not truncated into wrong counts or out-of-bounds parsing. * proc: fix mdstat obsolete chart lookup key mismatch Chart creation in proc_mdstat.c builds the short chart id "<raid>_<suffix>" and passes it to rrdset_create_localhost("mdstat", id, ...); the RRD layer later prefixes "mdstat." when constructing the full chart id. make_chart_obsolete() was instead formatting the full "mdstat.<raid>_<suffix>" directly into a 50-byte buffer and calling rrdset_find_active_byname_localhost, which means the two paths used different truncation boundaries. For long but valid md names (mdadm(8) allows up to 32 characters), the create-path chart was longer than what obsolete-path could build, so the lookup missed and the array's "availability" chart was never marked obsolete. Build the same short chart id in the obsolete path and resolve it through the type/id lookup helper so long valid md names match the created chart. Related: Coverity CID 414643 flagged an OVERRUN on this line; the trace is a tool-model false positive (no OOB in the current code), but investigating it surfaced the real correctness bug fixed here. * proc.plugin: avoid uaf in power supply property loop Coverity CID 348628 (USE_AFTER_FREE): do_sys_class_power_supply() could free the current power supply while iterating its property list, then evaluate the outer loop increment on the freed property node. Store the next property before the inner loop and stop iterating once the error path frees the power supply. * rrdhost: fix unlocked receiver status snapshot Coverity CID 410067 (MISSING_LOCK): rrdhost_status_ingest() read receiver status fields without receiver_lock while the receiver thread updated the same state under that lock. Snapshot the receiver status block under the lock before deriving ingest status so status reporting no longer races with connect and disconnect updates. * aclk: fix pending request cancellation race Coverity CID 410087 (MISSING_LOCK): aclk_web_client_interrupt_cb() read pending_req_list.canceled from a libuv worker while cancel paths updated the same flag from the ACLK side. Switch the cancel flag accesses to atomic load/store so query cancellation stays lock-free on the hot callback path without racing across threads. * statsd: lock histogram sample buffer access Coverity CID 410124 (MISSING_LOCK): histogram and timer samples updated the shared buffer partly outside `m->histogram.ext->mutex` while the flush path sorted the same buffer under that lock. Keep reset, growth, append, and flush snapshot on the same mutex so samples are not dropped or read from a partially synchronized buffer. * apps.plugin: unlock mutex before print exit Coverity CID 381151 (LOCK): the stored trace points to stale line numbers, but the current print-tree exit path still called exit() while holding apps_and_stdout_mutex. Unlock the mutex after printing and before exit() so the module destructor does not destroy a locked uv_mutex_t. --------- Co-authored-by: Costa Tsaousis <costa@netdata.cloud>

Stelios Fragkakis committed Apr 25, 2026 at 16:03 UTC eee9c5527987c94ff8321877e2e673780c71b534
10 files changed +116 -47
src/aclk/aclk_query.c
+3 -3
@@ -71,7 +71,7 @@ void mark_pending_req_cancel_all()
71 spinlock_lock(&pending_req_list_lock);
72 struct pending_req_list *curr = pending_req_list_head;
73 while (curr) {
74 - curr->canceled = 1;
74 + __atomic_store_n(&curr->canceled, 1, __ATOMIC_RELAXED);
75 curr = curr->next;
76 }
77 spinlock_unlock(&pending_req_list_lock);
@@ -86,7 +86,7 @@ int mark_pending_req_cancelled(const char *msg_id)
86
87 while (curr) {
88 if (curr->hash == hash && strcmp(curr->msg_id, msg_id) == 0) {
89 - curr->canceled = 1;
89 + __atomic_store_n(&curr->canceled, 1, __ATOMIC_RELAXED);
90 spinlock_unlock(&pending_req_list_lock);
91 return 0;
92 }
@@ -100,7 +100,7 @@ int mark_pending_req_cancelled(const char *msg_id)
100 static bool aclk_web_client_interrupt_cb(struct web_client *w __maybe_unused, void *data)
101 {
102 struct pending_req_list *req = (struct pending_req_list *)data;
103 - return req->canceled;
103 + return __atomic_load_n(&req->canceled, __ATOMIC_RELAXED);
104 }
105
106 int http_api_v2(mqtt_wss_client client, aclk_query_t *query)
src/collectors/apps.plugin/apps_plugin.c
+1
@@ -857,6 +857,7 @@ int main(int argc, char **argv) {
857
858 if(unlikely(print_tree_and_exit)) {
859 print_hierarchy(root_of_pids());
860 + netdata_mutex_unlock(&apps_and_stdout_mutex);
861 exit(0);
862 }
863
src/collectors/proc.plugin/proc_mdstat.c
+2 -2
@@ -70,8 +70,8 @@ static inline void make_chart_obsolete(char *name, const char *id_modifier)
70 RRDSET *st = NULL;
71
72 if (likely(name && id_modifier)) {
73 - snprintfz(id, sizeof(id) - 1, "mdstat.%s_%s", name, id_modifier);
74 - st = rrdset_find_active_byname_localhost(id);
73 + snprintfz(id, sizeof(id) - 1, "%s_%s", name, id_modifier);
74 + st = rrdset_find_active_bytype_localhost("mdstat", id);
75 if (likely(st))
76 rrdset_is_obsolete___safe_from_collector_thread(st);
77 }
src/collectors/proc.plugin/sys_class_power_supply.c
+9 -1
@@ -274,7 +274,8 @@ int do_sys_class_power_supply(int update_every, usec_t dt) {
274 struct ps_property *pr;
275 if (likely(ps))
276 {
277 - for(pr = ps->property_root; pr && !read_error; pr = pr->next) {
277 + for(pr = ps->property_root; pr && !read_error; ) {
278 + struct ps_property *next = pr->next;
279 struct ps_property_dim *pd;
280 for(pd = pr->property_dim_root; pd; pd = pd->next) {
281 if(likely(!pd->always_zero)) {
@@ -286,6 +287,7 @@ int do_sys_class_power_supply(int update_every, usec_t dt) {
287 collector_error("Cannot open file '%s'", pd->filename);
288 read_error = 1;
289 power_supply_free(ps);
290 + ps = NULL;
291 break;
292 }
293 }
@@ -295,6 +297,7 @@ int do_sys_class_power_supply(int update_every, usec_t dt) {
297 collector_error("Cannot read file '%s'", pd->filename);
298 read_error = 1;
299 power_supply_free(ps);
300 + ps = NULL;
301 break;
302 }
303 buffer[r] = '\0';
@@ -311,6 +314,11 @@ int do_sys_class_power_supply(int update_every, usec_t dt) {
314 }
315 }
316 }
317 +
318 + if(unlikely(read_error))
319 + break;
320 +
321 + pr = next;
322 }
323 }
324 }
src/collectors/statsd.plugin/statsd.c
+36 -26
@@ -540,13 +540,15 @@ static inline void statsd_process_histogram_or_timer(STATSD_METRIC *m, const cha
540 return;
541 }
542
543 - if(unlikely(m->reset)) {
544 - m->histogram.ext->used = 0;
545 - statsd_reset_metric(m);
546 - }
547 -
543 if(unlikely(value_is_zinit(value))) {
544 // magic loading of metric, without affecting anything
545 +
546 + netdata_mutex_lock(&m->histogram.ext->mutex);
547 + if(unlikely(m->reset)) {
548 + m->histogram.ext->used = 0;
549 + statsd_reset_metric(m);
550 + }
551 + netdata_mutex_unlock(&m->histogram.ext->mutex);
552 }
553 else {
554 NETDATA_DOUBLE v = statsd_parse_float(value, 1.0);
@@ -555,18 +557,23 @@ static inline void statsd_process_histogram_or_timer(STATSD_METRIC *m, const cha
557 if(unlikely(isgreater(sampling_rate, 1.0))) sampling_rate = 1.0;
558
559 long long samples = llrintndd(1.0 / sampling_rate);
558 - while(samples-- > 0) {
560 + netdata_mutex_lock(&m->histogram.ext->mutex);
561
562 + if(unlikely(m->reset)) {
563 + m->histogram.ext->used = 0;
564 + statsd_reset_metric(m);
565 + }
566 +
567 + while(samples-- > 0) {
568 if(unlikely(m->histogram.ext->used == m->histogram.ext->size)) {
561 - netdata_mutex_lock(&m->histogram.ext->mutex);
569 m->histogram.ext->size += statsd.histogram_increase_step;
570 m->histogram.ext->values = reallocz(m->histogram.ext->values, sizeof(NETDATA_DOUBLE) * m->histogram.ext->size);
564 - netdata_mutex_unlock(&m->histogram.ext->mutex);
571 }
572
573 m->histogram.ext->values[m->histogram.ext->used++] = v;
574 }
575
576 + netdata_mutex_unlock(&m->histogram.ext->mutex);
577 metric_update_counters_and_obsoletion(m);
578 }
579 }
@@ -1972,29 +1979,32 @@ static inline void statsd_flush_timer_or_histogram(STATSD_METRIC *m, const char
1979 netdata_log_debug(D_STATSD, "flushing %s metric '%s'", dim, m->name);
1980
1981 int updated = 0;
1975 - if(unlikely(!m->reset && m->count && m->histogram.ext->used > 0)) {
1982 + if(unlikely(!m->reset && m->count)) {
1983 netdata_mutex_lock(&m->histogram.ext->mutex);
1984
1978 - size_t len = m->histogram.ext->used;
1979 - NETDATA_DOUBLE *series = m->histogram.ext->values;
1980 - sort_series(series, len);
1981 -
1982 - m->histogram.ext->last_min = (collected_number)roundndd(series[0] * statsd.decimal_detail);
1983 - m->histogram.ext->last_max = (collected_number)roundndd(series[len - 1] * statsd.decimal_detail);
1984 - m->last = (collected_number)roundndd(average(series, len) * statsd.decimal_detail);
1985 - m->histogram.ext->last_stddev = (collected_number)roundndd(standard_deviation(series, len) * statsd.decimal_detail);
1986 - m->histogram.ext->last_sum = (collected_number)roundndd(sum(series, len) * statsd.decimal_detail);
1987 - m->histogram.ext->last_median = (collected_number)roundndd(median_on_sorted_series(series, len) * statsd.decimal_detail);
1988 - m->histogram.ext->last_percentile = (collected_number)roundndd(percentile_on_sorted_series(series, len, statsd.histogram_percentile / 100) * statsd.decimal_detail);
1985 + if(likely(m->histogram.ext->used > 0)) {
1986 + size_t len = m->histogram.ext->used;
1987 + NETDATA_DOUBLE *series = m->histogram.ext->values;
1988 + sort_series(series, len);
1989 +
1990 + m->histogram.ext->last_min = (collected_number)roundndd(series[0] * statsd.decimal_detail);
1991 + m->histogram.ext->last_max = (collected_number)roundndd(series[len - 1] * statsd.decimal_detail);
1992 + m->last = (collected_number)roundndd(average(series, len) * statsd.decimal_detail);
1993 + m->histogram.ext->last_stddev = (collected_number)roundndd(standard_deviation(series, len) * statsd.decimal_detail);
1994 + m->histogram.ext->last_sum = (collected_number)roundndd(sum(series, len) * statsd.decimal_detail);
1995 + m->histogram.ext->last_median = (collected_number)roundndd(median_on_sorted_series(series, len) * statsd.decimal_detail);
1996 + m->histogram.ext->last_percentile = (collected_number)roundndd(percentile_on_sorted_series(series, len, statsd.histogram_percentile / 100) * statsd.decimal_detail);
1997 +
1998 + m->histogram.ext->zeroed = 0;
1999 + m->reset = 1;
2000 + updated = 1;
2001 + }
2002
2003 netdata_mutex_unlock(&m->histogram.ext->mutex);
2004
1992 - netdata_log_debug(D_STATSD, "STATSD %s metric %s: min " COLLECTED_NUMBER_FORMAT ", max " COLLECTED_NUMBER_FORMAT ", last " COLLECTED_NUMBER_FORMAT ", pcent " COLLECTED_NUMBER_FORMAT ", median " COLLECTED_NUMBER_FORMAT ", stddev " COLLECTED_NUMBER_FORMAT ", sum " COLLECTED_NUMBER_FORMAT,
1993 - dim, m->name, m->histogram.ext->last_min, m->histogram.ext->last_max, m->last, m->histogram.ext->last_percentile, m->histogram.ext->last_median, m->histogram.ext->last_stddev, m->histogram.ext->last_sum);
1994 -
1995 - m->histogram.ext->zeroed = 0;
1996 - m->reset = 1;
1997 - updated = 1;
2005 + if(updated)
2006 + netdata_log_debug(D_STATSD, "STATSD %s metric %s: min " COLLECTED_NUMBER_FORMAT ", max " COLLECTED_NUMBER_FORMAT ", last " COLLECTED_NUMBER_FORMAT ", pcent " COLLECTED_NUMBER_FORMAT ", median " COLLECTED_NUMBER_FORMAT ", stddev " COLLECTED_NUMBER_FORMAT ", sum " COLLECTED_NUMBER_FORMAT,
2007 + dim, m->name, m->histogram.ext->last_min, m->histogram.ext->last_max, m->last, m->histogram.ext->last_percentile, m->histogram.ext->last_median, m->histogram.ext->last_stddev, m->histogram.ext->last_sum);
2008 }
2009 else if(unlikely(!m->histogram.ext->zeroed)) {
2010 // reset the metrics
src/collectors/xenstat.plugin/xenstat_plugin.c
+1
@@ -389,6 +389,7 @@ static int xenstat_collect(xenstat_handle *xhandle, libxl_ctx *ctx, libxl_dominf
389 unsigned int id = xenstat_domain_id(domain);
390 if(unlikely(libxl_domain_info(ctx, info, id))) {
391 netdata_log_error("XENSTAT: cannot get domain info.");
392 + continue;
393 }
394 else {
395 snprintfz(uuid, LIBXL_UUID_FMTLEN, LIBXL_UUID_FMT "\n", LIBXL_UUID_BYTES(info->uuid));
src/database/rrdhost-status.c
+15 -5
@@ -149,8 +149,19 @@ static inline RRDHOST_INGEST_STATUS rrdhost_status_ingest(RRDHOST *host, RRDHOST
149 uint32_t collected_metrics = UINT32_MAX;
150 uint32_t replicating_instances = UINT32_MAX;
151
152 - time_t since = MAX(host->stream.rcv.status.last_connected, host->stream.rcv.status.last_disconnected);
153 - STREAM_HANDSHAKE reason = host->stream.rcv.status.reason;
152 + time_t last_connected;
153 + time_t last_disconnected;
154 + uint32_t connections;
155 + STREAM_HANDSHAKE reason;
156 +
157 + rrdhost_receiver_lock(host);
158 + last_connected = host->stream.rcv.status.last_connected;
159 + last_disconnected = host->stream.rcv.status.last_disconnected;
160 + connections = host->stream.rcv.status.connections;
161 + reason = host->stream.rcv.status.reason;
162 + rrdhost_receiver_unlock(host);
163 +
164 + time_t since = MAX(last_connected, last_disconnected);
165
166 if (online) {
167 if (db_status == RRDHOST_DB_STATUS_INITIALIZING)
@@ -169,7 +180,7 @@ static inline RRDHOST_INGEST_STATUS rrdhost_status_ingest(RRDHOST *host, RRDHOST
180 status = RRDHOST_INGEST_STATUS_ONLINE;
181 }
182 else {
172 - if(!host->stream.rcv.status.connections)
183 + if(!connections)
184 status = RRDHOST_INGEST_STATUS_ARCHIVED;
185 else
186 status = RRDHOST_INGEST_STATUS_OFFLINE;
@@ -215,7 +226,7 @@ static inline RRDHOST_INGEST_STATUS rrdhost_status_ingest(RRDHOST *host, RRDHOST
226 else
227 s->ingest.type = RRDHOST_INGEST_TYPE_ARCHIVED;
228
218 - s->ingest.id = host->stream.rcv.status.connections;
229 + s->ingest.id = connections;
230 }
231
232 return status;
@@ -395,4 +406,3 @@ RRDHOST_INGEST_STATUS rrdhost_get_ingest_status(RRDHOST *host, time_t now) {
406 RRDHOST_DB_STATUS db_status = rrdhost_status_db(host, now, NULL, flags, online);
407 return rrdhost_status_ingest(host, NULL, flags, db_status, online);
408 }
398 -
src/libnetdata/datetime/rfc3339.c
+13 -7
@@ -34,18 +34,24 @@ static inline size_t print_2digit(char *buffer, size_t size, int value) {
34 }
35
36 static inline size_t print_fraction(char *buffer, size_t size, usec_t fraction, size_t digits) {
37 - if (!buffer || size < digits) return 0;
37 + if (!buffer) return 0;
38
39 // Validate and cap the number of digits
40 digits = digits < 1 ? 1 : (digits > 9 ? 9 : digits);
41 + if (size < digits) return 0;
42
42 - // Calculate divisor to get correct precision
43 - usec_t divisor = 1;
44 - for (size_t i = 0; i < 6 - digits; i++)
45 - divisor *= 10;
43 + // Scale microseconds to the requested precision
44 + if (digits < 6) {
45 + usec_t divisor = 1;
46 + for (size_t i = digits; i < 6; i++)
47 + divisor *= 10;
48
47 - // Calculate the fraction to print
48 - fraction = fraction / divisor;
49 + fraction /= divisor;
50 + }
51 + else if (digits > 6) {
52 + for (size_t i = 6; i < digits; i++)
53 + fraction *= 10;
54 + }
55
56 // Ensure fraction won't exceed the requested number of digits
57 usec_t max_value = 1;
src/libnetdata/json/json-c-parser-unittest.c
+29
@@ -1452,6 +1452,34 @@ static int test_parse_txt2rfc3339(void) {
1452 return failed;
1453 }
1454
1455 +// ----------------------------------------------------------------------------
1456 +// RFC3339 formatter regression coverage
1457 +// ----------------------------------------------------------------------------
1458 +static int test_format_rfc3339(void) {
1459 + int failed = 0;
1460 + char buffer[RFC3339_MAX_LENGTH];
1461 + usec_t parsed;
1462 + size_t len;
1463 +
1464 + len = rfc3339_datetime_ut(buffer, sizeof(buffer), 123456, 3, true);
1465 + T(len == strlen("1970-01-01T00:00:00.123Z") && strcmp(buffer, "1970-01-01T00:00:00.123Z") == 0,
1466 + "format_rfc3339: 3 digits truncate microseconds");
1467 +
1468 + len = rfc3339_datetime_ut(buffer, sizeof(buffer), 123456, 7, true);
1469 + T(len == strlen("1970-01-01T00:00:00.1234560Z") && strcmp(buffer, "1970-01-01T00:00:00.1234560Z") == 0,
1470 + "format_rfc3339: 7 digits keep microseconds and pad trailing zero");
1471 + parsed = rfc3339_parse_ut(buffer, NULL);
1472 + T(parsed == 123456, "format_rfc3339: 7-digit output parses back to the same microseconds");
1473 +
1474 + len = rfc3339_datetime_ut(buffer, sizeof(buffer), 1, 9, true);
1475 + T(len == strlen("1970-01-01T00:00:00.000001000Z") && strcmp(buffer, "1970-01-01T00:00:00.000001000Z") == 0,
1476 + "format_rfc3339: 9 digits preserve leading zeros and pad nanoseconds");
1477 + parsed = rfc3339_parse_ut(buffer, NULL);
1478 + T(parsed == 1, "format_rfc3339: 9-digit output parses back to the same microseconds");
1479 +
1480 + return failed;
1481 +}
1482 +
1483 // ----------------------------------------------------------------------------
1484 // TXT2PATTERN — branches:
1485 // key found: string "*"→NULL, string other→string_strdupz,
@@ -2039,6 +2067,7 @@ int json_c_parser_unittest(void) {
2067 { "TXT2BUFFER", test_parse_txt2buffer },
2068 { "TXT2UUID", test_parse_txt2uuid },
2069 { "TXT2RFC3339", test_parse_txt2rfc3339 },
2070 + { "FORMAT_RFC3339", test_format_rfc3339 },
2071 { "TXT2PATTERN", test_parse_txt2pattern },
2072 { "TXT2ENUM", test_parse_txt2enum },
2073 { "ARRAY_OF_TXT2BITMAP", test_parse_array_of_txt2bitmap },
src/libnetdata/os/get_system_cpus.c
+7 -3
@@ -100,9 +100,13 @@ size_t os_read_cpuset_cpus(const char *filename, size_t system_cpus) {
100 static char *buf = NULL;
101 static size_t buf_size = 0;
102
103 - if(!buf) {
104 - buf_size = 100U + 6 * system_cpus + 1; // taken from kernel/cgroup/cpuset.c
105 - buf = mallocz(buf_size);
103 + if(unlikely(!system_cpus))
104 + system_cpus = os_get_system_cpus_uncached();
105 +
106 + size_t required_buf_size = 100U + 6 * system_cpus + 1; // taken from kernel/cgroup/cpuset.c
107 + if(unlikely(buf_size < required_buf_size)) {
108 + buf_size = required_buf_size;
109 + buf = reallocz(buf, buf_size);
110 }
111
112 int ret = read_txt_file(filename, buf, buf_size);