Open
fix(cve): 修复 CVE-2026-83601 在 netdata 中的漏洞 #49
fix(cve): 修复 CVE-2026-83601 在 netdata 中的漏洞 #49
Open
infra_teamAdd on Sep 26
Total: 2 files changed+288-1
@@ -0,0 +1,273 @@
1+From d8a86c37a9ef13e2816bed26272047dbb5548c34 Mon Sep 17 00:00:00 2001
2+From: infra_team <zhaiwenjie1@huawei.com>
3+Date: Fri, 25 Sep 2026 21:58:08 +0000
4+Subject: [PATCH] fix(pluginsd): add SLOT bounds check to prevent OOB write
5+ (CVE-2026-83601)
6+ 
7+Backport of upstream commit 034a774f689ac01488fc261ca729ce0875819b1b
8+(PR #22598) to netdata v2.2.6.
9+ 
10+- add PLUGINSD_CHART_SLOT_MAX (1000000) and PLUGINSD_DIMENSION_SLOT_MAX
11+ (65535) caps; SLOT values above the cap are ignored as uncached input
12+ and logged (rate-limited) instead of being used as a cache index
13+- pluginsd_parse_rrd_slot() takes a max_slot argument and validates the
14+ parsed value, rejecting sizes that would wrap the array allocation
15+- add the allocation overflow guard to pluginsd_rrddim_put_to_slot()
16+ (equivalent of upstream prd_array_create guard)
17+- port the pluginsd_parser_unittest_slot_bounds unit test
18+ 
19+Backport adaptation notes for v2.2.6:
20+ 
21+- src/database/rrdset-pluginsd-array.h does not exist in v2.2.6; the
22+ prd_array_create overflow guard is placed in the inline
23+ pluginsd_rrddim_put_to_slot() in src/plugins.d/pluginsd_internals.h
24+- v2 parser paths (pluginsd_begin_v2, pluginsd_set_v2) exist in v2.2.6
25+ only and get the same bounds check; upstream has no hunks for them
26+ 
27+Upstream-commit: https://github.com/netdata/netdata/commit/034a774f689ac01488fc261ca729ce0875819b1b
28+Signed-off-by: infra_team <zhaiwenjie1@huawei.com>
29+---
30+ src/plugins.d/pluginsd_internals.h | 36 +++++++++++--
31+ src/plugins.d/pluginsd_parser.c | 77 +++++++++++++++++++++++++---
32+ src/plugins.d/pluginsd_parser.h | 4 ++
33+ src/plugins.d/pluginsd_replication.c | 6 +--
34+ 4 files changed, 111 insertions(+), 12 deletions(-)
35+ 
36+diff --git a/src/plugins.d/pluginsd_internals.h b/src/plugins.d/pluginsd_internals.h
37+index c55f52a..8ab155b 100644
38+--- a/src/plugins.d/pluginsd_internals.h
39++++ b/src/plugins.d/pluginsd_internals.h
40+@@ -132,6 +132,13 @@ static inline void pluginsd_rrddim_put_to_slot(PARSER *parser, RRDSET *st, RRDDI
41+ }
42+
43+ if(wanted_size > st->pluginsd.size) {
44++ // size is expected to be bounded by the caller: pluginsd slot input is capped at
45++ // the parser (PLUGINSD_DIMENSION_SLOT_MAX) and the no-slots path uses the dimension
46++ // count, so the size multiplication below cannot overflow. The check documents and
47++ // guards that invariant against any future unbounded caller (debug builds only).
48++ internal_fatal(wanted_size > SIZE_MAX / sizeof(struct pluginsd_rrddim),
49++ "PLUGINSD: requested size %zu would overflow the allocation", wanted_size);
50++
51+ st->pluginsd.prd_array = reallocz(st->pluginsd.prd_array, wanted_size * sizeof(struct pluginsd_rrddim));
52+
53+ // initialize the empty slots
54+@@ -265,13 +272,36 @@ static inline RRDSET *pluginsd_find_chart(RRDHOST *host, const char *chart, cons
55+ return st;
56+ }
57+
58+-static ALWAYS_INLINE ssize_t pluginsd_parse_rrd_slot(char **words, size_t num_words) {
59++static ALWAYS_INLINE ssize_t pluginsd_parse_rrd_slot(char **words, size_t num_words, size_t max_slot) {
60++ // Contract: max_slot must fit in ssize_t so the (ssize_t) cast below stays
61++ // non-negative. All callers pass small compile-time caps (PLUGINSD_*_SLOT_MAX),
62++ // so for the inlined constant this check is folded away by the compiler.
63++ internal_fatal(max_slot > (size_t)SSIZE_MAX,
64++ "PLUGINSD: max_slot %zu exceeds SSIZE_MAX", max_slot);
65++
66+ ssize_t slot = -1;
67+ char *id = get_word(words, num_words, 1);
68++ // Words are NUL-terminated and && short-circuits left-to-right, so each id[k]
69++ // is read only after id[0..k-1] matched the non-NUL "SLOT" chars. A shorter word
70++ // (e.g. "X" or "SLOT") fails an earlier comparison or hits the terminator at id[4],
71++ // so these fixed-index reads never go past the token's NUL. No bounds check needed.
72+ if(id && id[0] == PLUGINSD_KEYWORD_SLOT[0] && id[1] == PLUGINSD_KEYWORD_SLOT[1] &&
73+ id[2] == PLUGINSD_KEYWORD_SLOT[2] && id[3] == PLUGINSD_KEYWORD_SLOT[3] && id[4] == ':') {
74+- slot = (ssize_t) str2ull_encoded(&id[5]);
75+- if(slot < 0) slot = 0; // to make the caller increment its idx of the words
76++ unsigned long long parsed_slot = str2ull_encoded(&id[5]);
77++ if(unlikely(parsed_slot > max_slot)) {
78++ nd_log_limit_static_global_var(erl_slot, 1, 0);
79++ nd_log_limit(&erl_slot, NDLS_COLLECTORS, NDLP_WARNING,
80++ "PLUGINSD: ignoring invalid SLOT value '%s' above the supported maximum %zu",
81++ &id[5], max_slot);
82++ // slot 0 means: the SLOT word was present (so the caller still advances its
83++ // word index), but the value is unusable as a cache index. Each caller
84++ // handles slot < 1 per its own path -- chart lookups fall back to finding
85++ // the chart by id, and dimension caching switches to the non-slotted
86++ // (by-position) path (pluginsd_rrddim_put_to_slot sets dims_with_slots=false).
87++ slot = 0;
88++ }
89++ else
90++ slot = (ssize_t)parsed_slot;
91+ }
92+
93+ return slot;
94+diff --git a/src/plugins.d/pluginsd_parser.c b/src/plugins.d/pluginsd_parser.c
95+index a0ab25a..d543ce6 100644
96+--- a/src/plugins.d/pluginsd_parser.c
97++++ b/src/plugins.d/pluginsd_parser.c
98+@@ -6,7 +6,7 @@
99+
100+ static inline PARSER_RC pluginsd_set(char **words, size_t num_words, PARSER *parser) {
101+ int idx = 1;
102+- ssize_t slot = pluginsd_parse_rrd_slot(words, num_words);
103++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, PLUGINSD_DIMENSION_SLOT_MAX);
104+ if(slot >= 0) idx++;
105+
106+ char *dimension = get_word(words, num_words, idx++);
107+@@ -35,7 +35,7 @@ static inline PARSER_RC pluginsd_set(char **words, size_t num_words, PARSER *par
108+
109+ static inline PARSER_RC pluginsd_begin(char **words, size_t num_words, PARSER *parser) {
110+ int idx = 1;
111+- ssize_t slot = pluginsd_parse_rrd_slot(words, num_words);
112++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, PLUGINSD_CHART_SLOT_MAX);
113+ if(slot >= 0) idx++;
114+
115+ char *id = get_word(words, num_words, idx++);
116+@@ -259,7 +259,7 @@ static inline PARSER_RC pluginsd_chart(char **words, size_t num_words, PARSER *p
117+ if(!host) return PLUGINSD_DISABLE_PLUGIN(parser, NULL, NULL);
118+
119+ int idx = 1;
120+- ssize_t slot = pluginsd_parse_rrd_slot(words, num_words);
121++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, PLUGINSD_CHART_SLOT_MAX);
122+ if(slot >= 0) idx++;
123+
124+ char *type = get_word(words, num_words, idx++);
125+@@ -378,7 +378,7 @@ static inline PARSER_RC pluginsd_chart(char **words, size_t num_words, PARSER *p
126+
127+ static inline PARSER_RC pluginsd_dimension(char **words, size_t num_words, PARSER *parser) {
128+ int idx = 1;
129+- ssize_t slot = pluginsd_parse_rrd_slot(words, num_words);
130++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, PLUGINSD_DIMENSION_SLOT_MAX);
131+ if(slot >= 0) idx++;
132+
133+ char *id = get_word(words, num_words, idx++);
134+@@ -702,7 +702,7 @@ static ALWAYS_INLINE PARSER_RC pluginsd_begin_v2(char **words, size_t num_words,
135+ timing_init();
136+
137+ int idx = 1;
138+- ssize_t slot = pluginsd_parse_rrd_slot(words, num_words);
139++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, PLUGINSD_CHART_SLOT_MAX);
140+ if(slot >= 0) idx++;
141+
142+ char *id = get_word(words, num_words, idx++);
143+@@ -842,7 +842,7 @@ static ALWAYS_INLINE PARSER_RC pluginsd_set_v2(char **words, size_t num_words, P
144+ timing_init();
145+
146+ int idx = 1;
147+- ssize_t slot = pluginsd_parse_rrd_slot(words, num_words);
148++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, PLUGINSD_DIMENSION_SLOT_MAX);
149+ if(slot >= 0) idx++;
150+
151+ char *dimension = get_word(words, num_words, idx++);
152+@@ -1321,7 +1321,72 @@ void parser_init_repertoire(PARSER *parser, PARSER_REPERTOIRE repertoire) {
153+ }
154+ }
155+
156++static int pluginsd_parser_unittest_slot_bounds(size_t max_slot) {
157++ // The boundary cases below build "max_slot - 1", so a zero cap would underflow.
158++ // All real callers pass nonzero compile-time caps; guard against misuse anyway.
159++ if(max_slot < 1) {
160++ netdata_log_error("PLUGINSD: slot bounds unittest requires max_slot >= 1, got %zu", max_slot);
161++ return 1;
162++ }
163++
164++ // Note on initialization: every element below is given an explicit
165++ // initializer, so C zero-fills the remainder of each slot_word array. The
166++ // trailing three entries start empty and are filled from max_slot at runtime.
167++ struct slot_test_case {
168++ char slot_word[64];
169++ ssize_t expected;
170++ } cases[] = {
171++ { "", -1 }, // no SLOT word -> -1 (caller must not advance idx)
172++ { PLUGINSD_KEYWORD_SLOT ":0", 0 }, // explicit zero -> uncached
173++ { PLUGINSD_KEYWORD_SLOT ":1", 1 }, // smallest cached slot
174++ { PLUGINSD_KEYWORD_SLOT ":-1", 0 }, // negative parses as unsigned 0 -> uncached
175++ { PLUGINSD_KEYWORD_SLOT ":abc", 0 }, // malformed decimal -> 0 -> uncached
176++ { PLUGINSD_KEYWORD_SLOT ":0xZZ", 0 }, // malformed hex -> 0 -> uncached
177++ { PLUGINSD_KEYWORD_SLOT ":0x0AAAAAAAAAAAAAAB", 0 }, // over cap; cast stays positive, would wrap allocation
178++ { PLUGINSD_KEYWORD_SLOT ":0xFFFFFFFFFFFFFFFF", 0 }, // u64 max -> over cap -> uncached
179++ { PLUGINSD_KEYWORD_SLOT ":0x40000000", 0 }, // over both caps -> uncached (the reported OOM value)
180++ { "", 0 }, // filled below: max_slot - 1 (accepted)
181++ { "", 0 }, // filled below: max_slot (accepted, boundary)
182++ { "", 0 }, // filled below: max_slot + 1 (rejected, boundary)
183++ };
184++
185++ const size_t n = _countof(cases);
186++
187++ snprintfz(cases[n - 3].slot_word, sizeof(cases[n - 3].slot_word),
188++ PLUGINSD_KEYWORD_SLOT ":%zu", max_slot - 1);
189++ cases[n - 3].expected = (ssize_t)(max_slot - 1);
190++
191++ snprintfz(cases[n - 2].slot_word, sizeof(cases[n - 2].slot_word),
192++ PLUGINSD_KEYWORD_SLOT ":%zu", max_slot);
193++ cases[n - 2].expected = (ssize_t)max_slot;
194++
195++ snprintfz(cases[n - 1].slot_word, sizeof(cases[n - 1].slot_word),
196++ PLUGINSD_KEYWORD_SLOT ":%zu", max_slot + 1);
197++ cases[n - 1].expected = 0;
198++
199++ for(size_t i = 0; i < _countof(cases); i++) {
200++ char command[] = "DIMENSION";
201++ char *words[] = { command, cases[i].slot_word[0] ? cases[i].slot_word : NULL };
202++ size_t num_words = words[1] ? 2 : 1;
203++
204++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, max_slot);
205++ if(slot != cases[i].expected) {
206++ netdata_log_error("PLUGINSD: slot parser unittest failed for '%s': expected %zd, got %zd",
207++ words[1] ? words[1] : "(unset)", cases[i].expected, slot);
208++ return 1;
209++ }
210++ }
211++
212++ return 0;
213++}
214++
215+ int pluginsd_parser_unittest(void) {
216++ if(pluginsd_parser_unittest_slot_bounds(PLUGINSD_DIMENSION_SLOT_MAX))
217++ return 1;
218++
219++ if(pluginsd_parser_unittest_slot_bounds(PLUGINSD_CHART_SLOT_MAX))
220++ return 1;
221++
222+ PARSER *p = parser_init(NULL, -1, -1, PARSER_INPUT_SPLIT, NULL);
223+ pluginsd_keywords_init(p, PARSER_INIT_PLUGINSD | PARSER_INIT_STREAMING);
224+
225+diff --git a/src/plugins.d/pluginsd_parser.h b/src/plugins.d/pluginsd_parser.h
226+index e165275..54a06e5 100644
227+--- a/src/plugins.d/pluginsd_parser.h
228++++ b/src/plugins.d/pluginsd_parser.h
229+@@ -19,6 +19,10 @@
230+
231+ #define PLUGINSD_MIN_RRDSET_POINTERS_CACHE 1024
232+
233++// Slots are cache indexes. Larger values are treated as uncached input to avoid sparse cache allocations.
234++#define PLUGINSD_CHART_SLOT_MAX 1000000
235++#define PLUGINSD_DIMENSION_SLOT_MAX 65535
236++
237+ #define HOST_LABEL_IS_EPHEMERAL "_is_ephemeral"
238+ // PARSER return codes
239+ typedef enum __attribute__ ((__packed__)) parser_rc {
240+diff --git a/src/plugins.d/pluginsd_replication.c b/src/plugins.d/pluginsd_replication.c
241+index e8308e8..f7c7a8b 100644
242+--- a/src/plugins.d/pluginsd_replication.c
243++++ b/src/plugins.d/pluginsd_replication.c
244+@@ -111,7 +111,7 @@ PARSER_RC pluginsd_chart_definition_end(char **words, size_t num_words, PARSER *
245+
246+ ALWAYS_INLINE PARSER_RC pluginsd_replay_begin(char **words, size_t num_words, PARSER *parser) {
247+ int idx = 1;
248+- ssize_t slot = pluginsd_parse_rrd_slot(words, num_words);
249++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, PLUGINSD_CHART_SLOT_MAX);
250+ if(slot >= 0) idx++;
251+
252+ char *id = get_word(words, num_words, idx++);
253+@@ -216,7 +216,7 @@ ALWAYS_INLINE PARSER_RC pluginsd_replay_begin(char **words, size_t num_words, PA
254+
255+ ALWAYS_INLINE PARSER_RC pluginsd_replay_set(char **words, size_t num_words, PARSER *parser) {
256+ int idx = 1;
257+- ssize_t slot = pluginsd_parse_rrd_slot(words, num_words);
258++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, PLUGINSD_DIMENSION_SLOT_MAX);
259+ if(slot >= 0) idx++;
260+
261+ char *dimension = get_word(words, num_words, idx++);
262+@@ -295,7 +295,7 @@ ALWAYS_INLINE PARSER_RC pluginsd_replay_rrddim_collection_state(char **words, si
263+ return PARSER_RC_OK;
264+
265+ int idx = 1;
266+- ssize_t slot = pluginsd_parse_rrd_slot(words, num_words);
267++ ssize_t slot = pluginsd_parse_rrd_slot(words, num_words, PLUGINSD_DIMENSION_SLOT_MAX);
268+ if(slot >= 0) idx++;
269+
270+ char *dimension = get_word(words, num_words, idx++);
271+--
272+2.39.5
273+ 
@@ -13,6 +13,12 @@
13# Workaround for Missing build-id on go.d.plugin13# Workaround for Missing build-id on go.d.plugin
14%global _missing_build_ids_terminate_build 014%global _missing_build_ids_terminate_build 0
15 15 
16+# Builders grant only a small cpu quota and ~10G memory, but rpm still sees
17+# the host CPU count (ninja -j64/-j48 was observed), so the unbounded parallel
18+# compile of the big protobuf C++ sources and of go.d.plugin gets OOM-killed.
19+# Cap compile parallelism, and the go build parallelism (GOMAXPROCS) in %build.
20+%define _smp_build_ncpus 2
21+ 
16# We use some plugins which need suid grants22# We use some plugins which need suid grants
17%global _hardened_build 123%global _hardened_build 1
18 24 
@@ -21,7 +27,7 @@
21 27 
22Name: netdata28Name: netdata
23Version: 2.2.629Version: 2.2.6
24-Release: 330+Release: 4
25Summary: Real-time performance monitoring31Summary: Real-time performance monitoring
26# For a breakdown of the licensing, see license REDISTRIBUTED.md32# For a breakdown of the licensing, see license REDISTRIBUTED.md
27License: GPL-3.0-or-later33License: GPL-3.0-or-later
@@ -38,6 +44,7 @@ Source20: go.d.plugin-vendor-%{version}.tar.gz
38Patch0: netdata-fix-shebang-2.2.0.patch44Patch0: netdata-fix-shebang-2.2.0.patch
39Patch1: netdata-remove-web-v2.patch45Patch1: netdata-remove-web-v2.patch
40Patch2: fix-dbengine-repair-W-createdataset-23758.patch46Patch2: fix-dbengine-repair-W-createdataset-23758.patch
47+Patch3: fix-pluginsd-slot-bounds-CVE-2026-83601.patch
41BuildRequires: zlib-devel48BuildRequires: zlib-devel
42BuildRequires: git49BuildRequires: git
43BuildRequires: cmake50BuildRequires: cmake
@@ -162,6 +169,7 @@ go plugin for netdata
162%patch -P0 -p1169%patch -P0 -p1
163%patch -P1 -p1170%patch -P1 -p1
164%patch -P2 -p1171%patch -P2 -p1
172+%patch -P3 -p1
165# Remove closed source parts if present173# Remove closed source parts if present
166if [ -d src/web/gui/v2 ] ; then174if [ -d src/web/gui/v2 ] ; then
167 rm -rf src/web/gui/v2 src/web/gui/index.html175 rm -rf src/web/gui/v2 src/web/gui/index.html
@@ -236,6 +244,9 @@ sed -i -e '/set(BINDIR usr\/sbin)/s/sbin/bin/' CMakeLists.txt
236 -DENABLE_BUNDLED_JSONC=Off \244 -DENABLE_BUNDLED_JSONC=Off \
237 -DENABLE_BUNDLED_YAML=Off245 -DENABLE_BUNDLED_YAML=Off
238 246 
247+# go build derives its -p from GOMAXPROCS, i.e. the builder's host CPU count,
248+# which would start dozens of concurrent compiler processes on big hosts.
249+export GOMAXPROCS=2
239%{cmake_build}250%{cmake_build}
240 251 
241%install252%install
@@ -413,6 +424,9 @@ echo "Netdata config should be edited with %{_libexecdir}/%{name}/edit-config"
413 424 
414 425 
415%changelog426%changelog
427+* Sat Sep 26 2026 infra_team <zhaiwenjie1@huawei.com> - 2.2.6-4
428+- fix CVE-2026-83601: add SLOT bounds check in pluginsd parser to prevent out-of-bounds write
429+ 
416* Mon Sep 07 2026 xu_ping <707078654@qq.com> - 2.2.6-3430* Mon Sep 07 2026 xu_ping <707078654@qq.com> - 2.2.6-3
417- fix netdata -w coredump431- fix netdata -w coredump
418 432