Open
fix(cve): 修复 CVE-2026-19668 在 bind 中的漏洞 #347
fix(cve): 修复 CVE-2026-19668 在 bind 中的漏洞 #347
Open
infra_teamAdd on Sep 17
Total: 2 files changed+480-1
@@ -0,0 +1,472 @@
1+From: infra_team <zhaiwenjie1@huawei.com>
2+Date: Thu, 17 Sep 2026 12:00:00 +0800
3+Subject: [PATCH] fix CVE-2026-19668: bound the DS-to-DNSKEY matching during DNSSEC validation
4+ 
5+Backport the upstream fix for CVE-2026-19668 (ISC BIND 9.20.29 / 9.21.26,
6+Resource Exhaustion via Excessive DNSSEC Cryptographic Material Matching,
7+GitLab #5349).
8+ 
9+A securely delegated zone can serve a DS RRset and a DNSKEY RRset that
10+together hold many records with distinct key tags but no valid pairing.
11+Validating the DNSKEY RRset then walks every DS and, for each one, derives
12+key tags across the whole DNSKEY RRset -- matching work that was never
13+counted against any limit, so an attacker-controlled zone could force a
14+large amount of it per query.
15+ 
16+This backport charges each DS that matches no DNSKEY against the per-fetch
17+validation quota so the walk stops once the quota is reached, rejects a
18+DNSKEY RRset up front when the number of DS-by-DNSKEY combinations exceeds
19+a small multiple of that quota
20+(DS_DNSKEY_COMBINATIONS_PER_VALIDATION = 2), and makes an exhausted quota
21+terminal instead of relabelling it as a broken trust chain or retrying,
22+with the specific quota reason logged once at the common validator
23+completion point.
24+ 
25+Upstream changes are commits ff3fa30f2dcaa6 ("Bound the DS-to-DNSKEY
26+matching during DNSSEC validation") and ce25d582b475d1 ("Make the
27+exhausted DNSSEC validation quota terminal"), merged upstream in
28+a0a61dba9e1d4537b9e948b03a2879ba65000274 for the 9.20 branch.
29+ 
30+Adaptation for 9.20.21: upstream renamed validate_helper_run() to
31+validate_work_enqueue() after 9.20.21; the two hunks touching
32+validate_dnskey_dsset_first() / validate_dnskey_dsset_next_done() were
33+rebased onto the 9.20.21 validate_helper_run()/validate_async_run()
34+naming, keeping 9.20.21 behaviour otherwise unchanged. The test-only
35+commits (e0079f0a, ced8e955) are not backported: the 9.20.21 tarball
36+ships neither the bin/tests/system/dnssec_py suite nor
37+bin/tests/system/dnssec/tests_validation.py that they extend, and the
38+RPM build does not run the upstream system test suite.
39+ 
40+Upstream-commit: https://github.com/isc-projects/bind9/commit/a0a61dba9e1d4537b9e948b03a2879ba65000274
41+Upstream-commit: https://github.com/isc-projects/bind9/commit/ff3fa30f2dcaa6f48664f0ff4a5eef91f87bd364
42+Upstream-commit: https://github.com/isc-projects/bind9/commit/ce25d582b475d1d4ee22f48463cb8c547678584b
43+Fixes: CVE-2026-19668
44+Signed-off-by: infra_team <zhaiwenjie1@huawei.com>
45+---
46+ lib/dns/include/dns/validator.h | 16 ++--
47+ lib/dns/resolver.c | 63 ++++++++++------
48+ lib/dns/validator.c | 157 ++++++++++++++++++++++++++++++----------
49+ 3 files changed, 167 insertions(+), 69 deletions(-)
50+diff --git a/lib/dns/include/dns/validator.h b/lib/dns/include/dns/validator.h
51+index 362e7e7..2ba2f76 100644
52+--- a/lib/dns/include/dns/validator.h
53++++ b/lib/dns/include/dns/validator.h
54+@@ -142,18 +142,18 @@ struct dns_validator {
55+ dns_fixedname_t wild;
56+ dns_fixedname_t closest;
57+ ISC_LINK(dns_validator_t) link;
58+- bool mustbesecure;
59+- unsigned int depth;
60+- unsigned int authcount;
61+- unsigned int authfail;
62+- isc_stdtime_t start;
63+-
64++ bool mustbesecure;
65++ unsigned int depth;
66++ unsigned int authcount;
67++ unsigned int authfail;
68++ isc_stdtime_t start;
69++ bool resume;
70+ bool digest_sha1;
71+ uint8_t unsupported_algorithm;
72+ uint8_t unsupported_digest;
73+- uint8_t validation_attempts;
74++ uint16_t matchds_attempts;
75++ uint16_t validation_attempts;
76+ dns_rdata_t rdata;
77+- bool resume;
78+ isc_counter_t *nvalidations;
79+ isc_counter_t *nfails;
80+ isc_counter_t *qc;
81+diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
82+index aa7ffc4..27754a0 100644
83+--- a/lib/dns/resolver.c
84++++ b/lib/dns/resolver.c
85+@@ -5452,27 +5452,14 @@ validated(void *arg) {
86+ inc_stats(res, dns_resstatscounter_valfail);
87+ fctx->valfail++;
88+ fctx->vresult = val->result;
89+- if (fctx->vresult != DNS_R_BROKENCHAIN) {
90+- result = ISC_R_NOTFOUND;
91+- if (val->rdataset != NULL) {
92+- result = dns_db_findnode(fctx->cache, val->name,
93+- false, &node);
94+- }
95+- if (result == ISC_R_SUCCESS) {
96+- (void)dns_db_deleterdataset(fctx->cache, node,
97+- NULL, val->type, 0);
98+- }
99+- if (result == ISC_R_SUCCESS && val->sigrdataset != NULL)
100+- {
101+- (void)dns_db_deleterdataset(
102+- fctx->cache, node, NULL,
103+- dns_rdatatype_rrsig, val->type);
104+- }
105+- if (result == ISC_R_SUCCESS) {
106+- dns_db_detachnode(fctx->cache, &node);
107++ switch (fctx->vresult) {
108++ case DNS_R_BROKENCHAIN:
109++ case ISC_R_CANCELED:
110++ case ISC_R_SHUTTINGDOWN:
111++ case ISC_R_QUOTA:
112++ if (negative) {
113++ break;
114+ }
115+- }
116+- if (fctx->vresult == DNS_R_BROKENCHAIN && !negative) {
117+ /*
118+ * Cache the data as pending for later
119+ * validation.
120+@@ -5496,6 +5483,27 @@ validated(void *arg) {
121+ if (result == ISC_R_SUCCESS) {
122+ dns_db_detachnode(fctx->cache, &node);
123+ }
124++ break;
125++ default:
126++ result = ISC_R_NOTFOUND;
127++ if (val->rdataset != NULL) {
128++ result = dns_db_findnode(fctx->cache, val->name,
129++ false, &node);
130++ }
131++ if (result == ISC_R_SUCCESS) {
132++ (void)dns_db_deleterdataset(fctx->cache, node,
133++ NULL, val->type, 0);
134++ }
135++ if (result == ISC_R_SUCCESS && val->sigrdataset != NULL)
136++ {
137++ (void)dns_db_deleterdataset(
138++ fctx->cache, node, NULL,
139++ dns_rdatatype_rrsig, val->type);
140++ }
141++ if (result == ISC_R_SUCCESS) {
142++ dns_db_detachnode(fctx->cache, &node);
143++ }
144++ break;
145+ }
146+ result = fctx->vresult;
147+ add_bad(fctx, message, addrinfo, result, badns_validation);
148+@@ -5511,10 +5519,21 @@ validated(void *arg) {
149+ } else if (sentresponse) {
150+ done = true;
151+ goto cleanup_fetchctx;
152+- } else if (result == DNS_R_BROKENCHAIN) {
153++ }
154++
155++ /*
156++ * A broken trust chain isn't recoverable, and neither is an
157++ * exhausted DNSSEC validation budget: retrying would only do
158++ * more validation work against the same quota.
159++ */
160++ switch (result) {
161++ case DNS_R_BROKENCHAIN:
162++ case ISC_R_CANCELED:
163++ case ISC_R_SHUTTINGDOWN:
164++ case ISC_R_QUOTA:
165+ done = true;
166+ goto cleanup_fetchctx;
167+- } else {
168++ default:
169+ fctx_try(fctx, true);
170+ goto cleanup_fetchctx;
171+ }
172+diff --git a/lib/dns/validator.c b/lib/dns/validator.c
173+index 49a3a1a..d63acb3 100644
174+--- a/lib/dns/validator.c
175++++ b/lib/dns/validator.c
176+@@ -121,6 +121,13 @@ enum valattr {
177+ #define MAXVALIDATIONFAILS(r) \
178+ (((r)->attributes & VALATTR_MAXVALIDATIONFAILS) != 0)
179+
180++/*
181++ * How many DS x DNSKEY matching combinations to allow per validation
182++ * permitted by max-validations-per-fetch; matching a DS against a DNSKEY
183++ * (a keytag computation) is far cheaper than a signature validation.
184++ */
185++#define DS_DNSKEY_COMBINATIONS_PER_VALIDATION 2
186++
187+ static void
188+ destroy_validator(dns_validator_t *val);
189+
190+@@ -161,6 +168,12 @@ validator_logcreate(dns_validator_t *val, dns_name_t *name,
191+ dns_rdatatype_t type, const char *caller,
192+ const char *operation);
193+
194++static void
195++validate_answer_finish(void *arg);
196++
197++static void
198++validator_cancel_finish(dns_validator_t *validator);
199++
200+ static isc_result_t
201+ create_fetch(dns_validator_t *val, dns_name_t *name, dns_rdatatype_t type,
202+ isc_job_cb callback, const char *caller);
203+@@ -437,17 +450,22 @@ static void
204+ resume_answer_with_key_done(void *arg) {
205+ dns_validator_t *val = arg;
206+
207++ val->attributes &= ~VALATTR_OFFLOADED;
208++ if (CANCELING(val)) {
209++ validator_cancel_finish(val);
210++ val->result = ISC_R_CANCELED;
211++ }
212++
213+ switch (val->result) {
214+ case ISC_R_CANCELED: /* Validation was canceled */
215+ case ISC_R_SHUTTINGDOWN: /* Server shutting down */
216+ case ISC_R_QUOTA: /* Validation fails quota reached */
217+- dns_validator_cancel(val);
218+- break;
219++ validate_answer_finish(val);
220++ return;
221+ default:
222++ resume_answer(val);
223+ break;
224+ }
225+-
226+- resume_answer(val);
227+ }
228+
229+ /*%
230+@@ -506,6 +524,12 @@ fetch_callback_dnskey(void *arg) {
231+ result = validate_async_run(val, resume_answer);
232+ }
233+ break;
234++ case ISC_R_CANCELED:
235++ case ISC_R_SHUTTINGDOWN:
236++ case ISC_R_QUOTA:
237++ /* Abort, abort, abort */
238++ result = eresult;
239++ break;
240+ default:
241+ validator_log(val, ISC_LOG_DEBUG(3),
242+ "fetch_callback_dnskey: got %s",
243+@@ -585,6 +609,12 @@ fetch_callback_ds(void *arg) {
244+ isc_result_totext(eresult));
245+ result = proveunsecure(val, false, false);
246+ break;
247++ case ISC_R_CANCELED:
248++ case ISC_R_SHUTTINGDOWN:
249++ case ISC_R_QUOTA:
250++ /* Abort, abort, abort */
251++ result = eresult;
252++ break;
253+ default:
254+ validator_log(val, ISC_LOG_DEBUG(3),
255+ "fetch_callback_ds: got %s",
256+@@ -637,6 +667,12 @@ fetch_callback_ds(void *arg) {
257+ */
258+ result = proveunsecure(val, false, true);
259+ break;
260++ case ISC_R_CANCELED:
261++ case ISC_R_SHUTTINGDOWN:
262++ case ISC_R_QUOTA:
263++ /* Abort, abort, abort */
264++ result = eresult;
265++ break;
266+ default:
267+ validator_log(val, ISC_LOG_DEBUG(3),
268+ "fetch_callback_ds: got %s",
269+@@ -1623,9 +1659,6 @@ validate_answer_process(void *arg);
270+ static void
271+ validate_answer_iter_done(dns_validator_t *val, isc_result_t result);
272+
273+-static void
274+-validator_cancel_finish(dns_validator_t *validator);
275+-
276+ static void
277+ validate_answer_iter_start(dns_validator_t *val) {
278+ isc_result_t result = ISC_R_SUCCESS;
279+@@ -1685,9 +1718,6 @@ cleanup:
280+ (void)validate_async_run(val, validate_answer_process);
281+ }
282+
283+-static void
284+-validate_answer_finish(void *arg);
285+-
286+ static void
287+ validate_answer_signing_key_done(void *arg);
288+
289+@@ -1854,18 +1884,7 @@ validate_answer_finish(void *arg) {
290+ validate_async_done(val, val->result);
291+ return;
292+ case ISC_R_QUOTA:
293+- if (MAXVALIDATIONS(val)) {
294+- validator_log(val, ISC_LOG_DEBUG(3),
295+- "maximum number of validations exceeded");
296+- } else if (MAXVALIDATIONFAILS(val)) {
297+- validator_log(val, ISC_LOG_DEBUG(3),
298+- "maximum number of validation failures "
299+- "exceeded");
300+- } else {
301+- validator_log(
302+- val, ISC_LOG_DEBUG(3),
303+- "unknown error: validation quota exceeded");
304+- }
305++ /* validate_async_done() logs the specific quota reason. */
306+ validate_async_done(val, val->result);
307+ return;
308+ default:
309+@@ -1946,6 +1965,22 @@ validate_helper_run(dns_validator_t *val, isc_job_cb cb) {
310+
311+ static void
312+ validate_async_done(dns_validator_t *val, isc_result_t result) {
313++ if (result == ISC_R_QUOTA) {
314++ /*
315++ * Log the reason on the validator that actually hit the quota
316++ * (it set the attribute); a parent that merely inherits the
317++ * quota result from a sub-validation stays quiet.
318++ */
319++ if (MAXVALIDATIONS(val)) {
320++ validator_log(val, ISC_LOG_DEBUG(3),
321++ "maximum number of validations exceeded");
322++ } else if (MAXVALIDATIONFAILS(val)) {
323++ validator_log(val, ISC_LOG_DEBUG(3),
324++ "maximum number of validation failures "
325++ "exceeded");
326++ }
327++ }
328++
329+ if (result == DNS_R_NOVALIDSIG &&
330+ (val->attributes & VALATTR_TRIEDVERIFY) == 0)
331+ {
332+@@ -2094,6 +2129,7 @@ validate_dnskey_dsset_done(dns_validator_t *val, isc_result_t result) {
333+ switch (result) {
334+ case ISC_R_CANCELED:
335+ case ISC_R_SHUTTINGDOWN:
336++ case ISC_R_QUOTA:
337+ /* Abort, abort, abort! */
338+ break;
339+ case ISC_R_SUCCESS:
340+@@ -2133,7 +2169,6 @@ validate_dnskey_dsset(dns_validator_t *val) {
341+ isc_result_t result;
342+ dns_rdata_ds_t ds;
343+
344+- dns_rdata_reset(&dsrdata);
345+ dns_rdataset_current(val->dsset, &dsrdata);
346+ result = dns_rdata_tostruct(&dsrdata, &ds, NULL);
347+ RUNTIME_CHECK(result == ISC_R_SUCCESS);
348+@@ -2160,7 +2195,31 @@ validate_dnskey_dsset(dns_validator_t *val) {
349+ return DNS_R_BADALG;
350+ }
351+
352+- val->validation_attempts++;
353++ val->matchds_attempts++;
354++
355++ /*
356++ * Matching one DS against the DNSKEY RRset derives a key tag for
357++ * every DNSKEY, so each DS that reaches this point costs one key-tag
358++ * computation per key. Bound the accumulated DS-by-DNSKEY work at
359++ * DS_DNSKEY_COMBINATIONS_PER_VALIDATION key tags per allowed
360++ * validation and stop once it is exceeded, so a flood of mismatched
361++ * (or matching but unsigned) DS records cannot force unbounded
362++ * matching. Ignored DS (unsupported digest or algorithm) return
363++ * above without being counted. A trust-anchor dsset is locally
364++ * configured, not attacker supplied, and its rdataset has no count
365++ * method.
366++ */
367++ if (val->nvalidations != NULL && val->dsset != &val->fdsset) {
368++ size_t keycount = dns_rdataset_count(val->rdataset);
369++
370++ if ((size_t)val->matchds_attempts * keycount >
371++ (size_t)isc_counter_getlimit(val->nvalidations) *
372++ DS_DNSKEY_COMBINATIONS_PER_VALIDATION)
373++ {
374++ val->attributes |= VALATTR_MAXVALIDATIONS;
375++ return ISC_R_QUOTA;
376++ }
377++ }
378+
379+ /*
380+ * Find the DNSKEY matching the DS...
381+@@ -2168,15 +2227,35 @@ validate_dnskey_dsset(dns_validator_t *val) {
382+ result = dns_dnssec_matchdskey(val->name, &dsrdata, val->rdataset,
383+ &keyrdata);
384+ if (result != ISC_R_SUCCESS) {
385++ val->validation_attempts++;
386+ validator_log(val, ISC_LOG_DEBUG(3), "no DNSKEY matching DS");
387++ /*
388++ * A DS that matches no DNSKEY is wasted key-tag matching
389++ * work; count it against the validation quota so a flood of
390++ * mismatched DS records cannot force unbounded matching.
391++ */
392++ consume_validation(val);
393++ if (over_max_validations(val)) {
394++ return ISC_R_QUOTA;
395++ }
396+ return DNS_R_NOKEYMATCH;
397+ }
398+
399++ val->validation_attempts++;
400++
401+ /*
402+ * ... and check that it signed the DNSKEY RRset.
403+ */
404+ result = check_signer(val, &keyrdata, ds.key_tag, ds.algorithm);
405+- if (result != ISC_R_SUCCESS) {
406++ switch (result) {
407++ case ISC_R_SUCCESS:
408++ break;
409++ case ISC_R_CANCELED:
410++ case ISC_R_SHUTTINGDOWN:
411++ case ISC_R_QUOTA:
412++ /* Abort, abort, abort */
413++ return result;
414++ default:
415+ validator_log(val, ISC_LOG_DEBUG(3),
416+ "no RRSIG matching DS key");
417+
418+@@ -2221,14 +2300,21 @@ validate_dnskey_dsset_next_done(void *arg) {
419+ switch (result) {
420+ case ISC_R_CANCELED:
421+ case ISC_R_SHUTTINGDOWN:
422++ case ISC_R_QUOTA:
423+ /* Abort, abort, abort! */
424+ break;
425+ case ISC_R_SUCCESS:
426+ case ISC_R_NOMORE:
427+- /* We are done */
428+ break;
429+ default:
430+- /* Continue validation until we have success or no more data */
431++ /*
432++ * A DS that matched no DNSKEY, or matched one that did not
433++ * sign the RRset (for example a standby KSK), is not a
434++ * validation failure: mismatched DS records are already
435++ * charged against the quota in validate_dnskey_dsset() and
436++ * signature failures inside verify(). Continue until we
437++ * have success or no more data.
438++ */
439+ (void)validate_helper_run(val, validate_dnskey_dsset_next);
440+ return;
441+ }
442+@@ -2244,25 +2330,18 @@ validate_dnskey_dsset_next_done(void *arg) {
443+
444+ static void
445+ validate_dnskey_dsset_first(dns_validator_t *val) {
446+- isc_result_t result;
447+-
448+ if (CANCELED(val) || CANCELING(val)) {
449+- result = ISC_R_CANCELED;
450++ val->result = ISC_R_CANCELED;
451+ } else {
452+- result = dns_rdataset_first(val->dsset);
453++ val->result = dns_rdataset_first(val->dsset);
454+ }
455+
456+- if (result == ISC_R_SUCCESS) {
457++ if (val->result == ISC_R_SUCCESS) {
458+ /* continue async run */
459+- result = validate_dnskey_dsset(val);
460+- if (result != ISC_R_SUCCESS) {
461+- (void)validate_helper_run(val,
462+- validate_dnskey_dsset_next);
463+- return;
464+- }
465++ val->result = validate_dnskey_dsset(val);
466+ }
467+
468+- validate_dnskey_dsset_done(val, result);
469++ (void)validate_async_run(val, validate_dnskey_dsset_next_done);
470+ }
471+
472+ static void
@@ -29,7 +29,7 @@ Summary: The Berkeley Internet Name Domain (BIND) DNS (Domain Name System) serv
29Name: bind29Name: bind
30License: MPLv2.030License: MPLv2.0
31Version: 9.20.2131Version: 9.20.21
32-Release: 432+Release: 5
33Epoch: 3233Epoch: 32
34Url: https://www.isc.org/downloads/bind/34Url: https://www.isc.org/downloads/bind/
35#35#
@@ -94,6 +94,7 @@ Patch6030:backport-CVE-2026-13204-01.patch
94Patch6031:backport-CVE-2026-13204-02.patch94Patch6031:backport-CVE-2026-13204-02.patch
95Patch6032:backport-CVE-2026-13204-03.patch95Patch6032:backport-CVE-2026-13204-03.patch
96Patch6033:backport-CVE-2026-11622.patch96Patch6033:backport-CVE-2026-11622.patch
97+Patch6034:backport-CVE-2026-19668-01.patch
97# Common patches98# Common patches
98 99 
99%{?systemd_ordering}100%{?systemd_ordering}
@@ -923,6 +924,12 @@ fi;
923%endif924%endif
924 925 
925%changelog926%changelog
927+* Thu Sep 17 2026 infra_team <zhaiwenjie1@huawei.com> - 32:9.20.21-5
928+- Type:CVE
929+- CVE:CVE-2026-19668
930+- SUG:NA
931+- DESC:fix CVE-2026-19668
932+ 
926* Sat Aug 01 2026 tangce <tangce1@h-partners.com> - 32:9.20.21-4933* Sat Aug 01 2026 tangce <tangce1@h-partners.com> - 32:9.20.21-4
927- Type:CVE934- Type:CVE
928- CVE:CVE-2026-10822,CVE-2026-11331,CVE-2026-12617,CVE-2026-13204,CVE-2026-11622935- CVE:CVE-2026-10822,CVE-2026-11331,CVE-2026-12617,CVE-2026-13204,CVE-2026-11622