From 2ec2afd6b06b77bf4142ae93084f5df711bedd6c Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Mon, 24 Aug 2026 16:06:17 +0200 Subject: [PATCH] - Fix #132: Add `harden-cname-follow: yes` config option, that can be used to disable cname scrubbing. That can be useful for when the traffic is on localhost only. --- daemon/remote.c | 1 + doc/Changelog | 5 ++ doc/example.conf.in | 3 + doc/unbound.conf.rst | 11 +++ iterator/iter_resptype.c | 20 ++++- iterator/iter_resptype.h | 7 +- iterator/iter_scrub.c | 28 ++++++- iterator/iter_utils.c | 25 +++++++ iterator/iter_utils.h | 3 + iterator/iterator.c | 45 ++++++++++- testdata/fwd_cname_follow.rpl | 137 ++++++++++++++++++++++++++++++++++ util/config_file.c | 3 + util/config_file.h | 2 + util/configlexer.lex | 1 + util/configparser.y | 14 +++- 15 files changed, 297 insertions(+), 8 deletions(-) create mode 100644 testdata/fwd_cname_follow.rpl diff --git a/daemon/remote.c b/daemon/remote.c index 6931a78f7..55bb710ee 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -6373,6 +6373,7 @@ fr_atomic_copy_cfg(struct config_file* oldcfg, struct config_file* cfg, COPY_VAR_int(harden_referral_path); COPY_VAR_int(harden_algo_downgrade); COPY_VAR_int(harden_unknown_additional); + COPY_VAR_int(harden_cname_follow); COPY_VAR_int(use_caps_bits_for_id); COPY_VAR_ptr(caps_whitelist); COPY_VAR_ptr(private_address); diff --git a/doc/Changelog b/doc/Changelog index 89c7f3380..a4eb9fe5b 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,3 +1,8 @@ +24 August 2026: Wouter + - Fix #132: Add `harden-cname-follow: yes` config option, that + can be used to disable cname scrubbing. That can be useful + for when the traffic is on localhost only. + 21 August 2026: Wouter - Fix to defend against double event deletion, that could cause event corruption and use after free, for comm_timer_disable. diff --git a/doc/example.conf.in b/doc/example.conf.in index c5e4e242e..603ba740c 100644 --- a/doc/example.conf.in +++ b/doc/example.conf.in @@ -582,6 +582,9 @@ server: # additional section. # harden-unknown-additional: no + # Harden CNAME redirections by following them. + # harden-cname-follow: yes + # Sent minimum amount of information to upstream servers to enhance # privacy. Only sent minimum required labels of the QNAME and set QTYPE # to A when possible. diff --git a/doc/unbound.conf.rst b/doc/unbound.conf.rst index e341c516f..2823971b6 100644 --- a/doc/unbound.conf.rst +++ b/doc/unbound.conf.rst @@ -1952,6 +1952,17 @@ These options are part of the ``server:`` section. Default: no +@@UAHL@unbound.conf@harden-cname-follow@@: ** + Harden CNAME redirections by following them. + If no, then upstream CNAME and DNAME redirections are allowed in a + response without checking with further messages if those are valid. + It can only really be disabled safely on localhost net or encrypted + connectivity. + Default is on to protect the cache integrity. + + Default: yes + + @@UAHL@unbound.conf@use-caps-for-id@@: ** Use 0x20-encoded random bits in the query to foil spoof attempts. This perturbs the lowercase and uppercase of query names sent to authority diff --git a/iterator/iter_resptype.c b/iterator/iter_resptype.c index f16520b3e..18501ff6a 100644 --- a/iterator/iter_resptype.c +++ b/iterator/iter_resptype.c @@ -46,6 +46,7 @@ #include "services/cache/dns.h" #include "util/net_help.h" #include "util/data/dname.h" +#include "util/config_file.h" #include "sldns/rrdef.h" #include "sldns/pkthdr.h" @@ -107,7 +108,8 @@ response_type_from_cache(struct dns_msg* msg, enum response_type response_type_from_server(int rdset, struct dns_msg* msg, struct query_info* request, struct delegpt* dp, - int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral) + int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral, + struct config_file* cfg) { uint8_t* origzone = (uint8_t*)"\000"; /* the default */ struct ub_packed_rrset_key* s; @@ -130,6 +132,16 @@ response_type_from_server(int rdset, if( (msg->rep->flags&BIT_RA) && !(msg->rep->flags&BIT_AA) && !rdset) return RESPONSE_TYPE_REC_LAME; + if(!cfg->harden_cname_follow /* follow CNAME chain */) { + /* If the CNAME chain is allowed, see if there is a + * SOA record, if so, the chain is complete to the + * end of it, otherwise it could be a partial chain.*/ + for(i=msg->rep->an_numrrsets; irep->an_numrrsets+msg->rep->ns_numrrsets; i++) { + s = msg->rep->rrsets[i]; + if(ntohs(s->rk.type) == LDNS_RR_TYPE_SOA) + return RESPONSE_TYPE_ANSWER; + } + } /* it could be a CNAME with NXDOMAIN rcode */ for(i=0; irep->an_numrrsets; i++) { s = msg->rep->rrsets[i]; @@ -162,6 +174,7 @@ response_type_from_server(int rdset, if(msg->rep->an_numrrsets > 0) { uint8_t* mname = request->qname; size_t mname_len = request->qname_len; + int sawanswer = 0; /* Now look at the answer section first. 3 states: our * answer is there directly, our answer is there after @@ -197,6 +210,7 @@ response_type_from_server(int rdset, * the answer, we only provisionally say * 'ANSWER' -- it very well could be a * REFERRAL. */ + sawanswer = 1; break; } @@ -214,6 +228,10 @@ response_type_from_server(int rdset, * still got to here, then it is a CNAME response. * (This is regardless of the AA bit at this point) */ if(mname != request->qname) { + if(!cfg->harden_cname_follow /* allow CNAMEs */ && + sawanswer /* The last was not CNAME, and + there is an answer RRset. */ ) + return RESPONSE_TYPE_ANSWER; return RESPONSE_TYPE_CNAME; } } diff --git a/iterator/iter_resptype.h b/iterator/iter_resptype.h index 73719abda..b6ebfe8d8 100644 --- a/iterator/iter_resptype.h +++ b/iterator/iter_resptype.h @@ -45,6 +45,7 @@ struct dns_msg; struct query_info; struct delegpt; +struct config_file; /** * The response type is used to interpret the response. @@ -115,7 +116,7 @@ enum response_type response_type_from_cache(struct dns_msg* msg, * detection, mostly). * * @param rdset: if RD bit was sent in query sent by unbound. - * @param msg: the message from the cache. + * @param msg: the message. * @param request: the request that generated the response. * @param dp: The delegation point that was being queried * when the response was returned. @@ -124,10 +125,12 @@ enum response_type response_type_from_cache(struct dns_msg* msg, * is lame, before it became empty. * @param msg_lame_referral: returned true if the reply has a referral before * scrub. + * @param cfg: config with options. * @return the response type (CNAME or ANSWER). */ enum response_type response_type_from_server(int rdset, struct dns_msg* msg, struct query_info* request, struct delegpt* dp, - int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral); + int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral, + struct config_file* cfg); #endif /* ITERATOR_ITER_RESPTYPE_H */ diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c index f2f20a5c1..c665adb78 100644 --- a/iterator/iter_scrub.c +++ b/iterator/iter_scrub.c @@ -517,7 +517,7 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, * server, scrub down the length to something * shorter. This deletes everything after the limit * is reached. The iterator is going to look up - * the content one by one anyway. */ + * the content one by one, if harden-cname-follow . */ remove_rrset("normalize: removing because too many cnames:", pkt, msg, prev, &rrset); continue; @@ -1021,6 +1021,8 @@ scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg, uint8_t* ns_rrset_dname = NULL; int added_rrlen_ede = 0; struct rrset_parse* rrset, *prev; + uint8_t* sname = qinfo->qname; + size_t snamelen = qinfo->qname_len; prev = NULL; rrset = msg->rrset_first; @@ -1028,6 +1030,7 @@ scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg, * it can be used from the cache. After normalization, an initial * DNAME will have a correctly synthesized CNAME after it. */ if(rrset && rrset->type == LDNS_RR_TYPE_DNAME && + env->cfg->harden_cname_follow /* CNAME chain is cut off, one DNAME is allowed here. */ && rrset->section == LDNS_SECTION_ANSWER && pkt_strict_sub(pkt, qinfo->qname, rrset->dname) && pkt_sub(pkt, rrset->dname, zonename)) { @@ -1043,12 +1046,33 @@ scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg, * ANY queries get query name in answer section. * Remainders of CNAME chains are cut off and resolved by iterator. */ while(rrset && rrset->section == LDNS_SECTION_ANSWER) { - if(dname_pkt_compare(pkt, qinfo->qname, rrset->dname) != 0) { + if(!env->cfg->harden_cname_follow /* CNAME chain is allowed to stay */ && + rrset->type == LDNS_RR_TYPE_DNAME && + pkt_strict_sub(pkt, sname, rrset->dname) && + pkt_sub(pkt, rrset->dname, zonename)) { + /* This DNAME is allowed to stay, the synthesized + * CNAME follows next. */ + prev = rrset; + rrset = rrset->rrset_all_next; + continue; + } + if(dname_pkt_compare(pkt, sname, rrset->dname) != 0) { if(has_additional(rrset->type)) del_addi = 1; remove_rrset("sanitize: removing extraneous answer " "RRset:", pkt, msg, prev, &rrset); continue; } + if(!env->cfg->harden_cname_follow /* CNAME chain is allowed to stay */ && + qinfo->qtype != LDNS_RR_TYPE_ANY && + rrset->type == LDNS_RR_TYPE_CNAME && + dname_pkt_compare(pkt, sname, rrset->dname) == 0) { + /* Follow the CNAME chain, and allow the elements + * that match the CNAME chain. Also allow a DNAME + * in front. */ + if(!parse_get_cname_target(rrset, &sname, &snamelen, + pkt)) + return 0; + } prev = rrset; rrset = rrset->rrset_all_next; } diff --git a/iterator/iter_utils.c b/iterator/iter_utils.c index e848e83ba..6c41656a2 100644 --- a/iterator/iter_utils.c +++ b/iterator/iter_utils.c @@ -1712,3 +1712,28 @@ deleg_port_number(struct module_env* env) return env->cfg->ssl_port; return -1; } + +void +shorten_answer_cname(struct reply_info* rep, uint8_t* cutoff) +{ + size_t i, found = 0, removenum; + uint8_t* sname = NULL; + size_t snamelen = 0; + for(i=0; ian_numrrsets; i++) { + if(query_dname_compare(rep->rrsets[i]->rk.dname, cutoff) == 0) { + found = 1; + removenum = i; + break; + } + if(ntohs(rep->rrsets[i]->rk.type) == LDNS_RR_TYPE_CNAME) { + get_cname_target(rep->rrsets[i], &sname, &snamelen); + if(query_dname_compare(sname, cutoff) == 0) { + found = 1; + removenum = i+1; + break; + } + } + } + if(!found) return; /* not found */ + val_reply_remove_answers(rep, removenum, rep->an_numrrsets-removenum); +} diff --git a/iterator/iter_utils.h b/iterator/iter_utils.h index 9fb361ab5..e5daca016 100644 --- a/iterator/iter_utils.h +++ b/iterator/iter_utils.h @@ -486,4 +486,7 @@ void iter_make_minimal(struct reply_info* rep); /** See if we need a different port number */ int deleg_port_number(struct module_env* env); +/** Shorten reply CNAME chain to cutoff, cutoff is excluded. */ +void shorten_answer_cname(struct reply_info* rep, uint8_t* cutoff); + #endif /* ITERATOR_ITER_UTILS_H */ diff --git a/iterator/iterator.c b/iterator/iterator.c index cadb95ebd..bb438af99 100644 --- a/iterator/iterator.c +++ b/iterator/iterator.c @@ -3189,7 +3189,7 @@ processQueryResponse(struct module_qstate* qstate, struct iter_qstate* iq, type = response_type_from_server( (int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd), iq->response, &iq->qinfo_out, iq->dp, &iq->empty_nodata_found, - iq->msg_lame_empty, iq->msg_lame_referral); + iq->msg_lame_empty, iq->msg_lame_referral, qstate->env->cfg); iq->chase_to_rd = 0; /* remove TC flag, if this is erroneously set by TCP upstream */ iq->response->rep->flags &= ~BIT_TC; @@ -3302,6 +3302,47 @@ processQueryResponse(struct module_qstate* qstate, struct iter_qstate* iq, * to send another query with a new qtype. */ type = RESPONSE_TYPE_ANSWER; } + if(type == RESPONSE_TYPE_ANSWER && + !qstate->env->cfg->harden_cname_follow /* cname chain from upstream is allowed */ && + qstate->env->auth_zones && + /* Check for CNAMEs in answer and RPZ after the CNAME. */ + reply_find_rrset_section_an( + iq->response->rep, iq->qchase.qname, + iq->qchase.qname_len, LDNS_RR_TYPE_CNAME, + iq->qchase.qclass) != NULL) { + /* If this is an answer with CNAMEs in front, and + * RPZ wants to modify after CNAME(s), cut off, the + * remainder, and treat as the CNAME response */ + size_t i; + uint8_t* origname = iq->qchase.qname; + size_t orignamelen = iq->qchase.qname_len; + for(i=0; iresponse->rep->an_numrrsets; i++) { + struct dns_msg* forged_response; + if(ntohs(iq->response->rep->rrsets[i]->rk.type) == + LDNS_RR_TYPE_DNAME) { + continue; + } + if(ntohs(iq->response->rep->rrsets[i]->rk.type) != + LDNS_RR_TYPE_CNAME) { + break; + } + get_cname_target(iq->response->rep->rrsets[i], + &iq->qchase.qname, &iq->qchase.qname_len); + forged_response = rpz_callback_from_iterator_cname(qstate, iq); + if(forged_response) { + /* Cut off the answer section at this point. + * RPZ is going to make an answer, in the + * processInit after the CNAME(s) in front + * are handled. */ + shorten_answer_cname(iq->response->rep, + iq->qchase.qname); + type = RESPONSE_TYPE_CNAME; + break; + } + } + iq->qchase.qname = origname; + iq->qchase.qname_len = orignamelen; + } /* handle each of the type cases */ if(type == RESPONSE_TYPE_ANSWER) { @@ -3804,7 +3845,7 @@ processPrimeResponse(struct module_qstate* qstate, int id) type = response_type_from_server( (int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd), iq->response, &iq->qchase, iq->dp, NULL, iq->msg_lame_empty, - iq->msg_lame_referral); + iq->msg_lame_referral, qstate->env->cfg); if(type == RESPONSE_TYPE_ANSWER) { qstate->return_rcode = LDNS_RCODE_NOERROR; qstate->return_msg = iq->response; diff --git a/testdata/fwd_cname_follow.rpl b/testdata/fwd_cname_follow.rpl new file mode 100644 index 000000000..b3aae17c6 --- /dev/null +++ b/testdata/fwd_cname_follow.rpl @@ -0,0 +1,137 @@ +; This is a comment. +; config options go here. +server: + # allow cname chains from the upstream. + harden-cname-follow: no + +forward-zone: name: "." forward-addr: 216.0.0.1 +CONFIG_END + +SCENARIO_BEGIN Forward and follow CNAME chain. +RANGE_BEGIN 0 100 + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR RD RA NOERROR +SECTION QUESTION +www.example.com. IN A +SECTION ANSWER +www.example.com. IN A 10.20.30.40 +SECTION AUTHORITY +www.example.com. IN NS ns.example.com. +www.example.com. IN NS ns.example.com. +www.example.com. IN NS ns.example.com. +SECTION ADDITIONAL +ns.example.com. IN A 10.20.30.50 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR RD RA NOERROR +SECTION QUESTION +www2.example.com. IN A +SECTION ANSWER +www2.example.com. IN CNAME b.ex2.com. +b.ex2.com. IN A 10.20.30.42 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR RD RA NOERROR +SECTION QUESTION +www3.example.com. IN A +SECTION ANSWER +www3.example.com. IN CNAME b.ex2.com. +b.ex2.com. IN CNAME b.ex3.com. +b.ex3.com. IN A 10.20.30.43 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR RD RA NOERROR +SECTION QUESTION +www4.example.com. IN A +SECTION ANSWER +www4.example.com. IN CNAME b4.ex2.com. +b4.ex2.com. IN CNAME b4.ex3.com. +b4.ex3.com. IN CNAME b4.ex4.com. +b4.ex4.com. IN A 10.20.30.44 +ENTRY_END + +RANGE_END + +STEP 1 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +www.example.com. IN A +ENTRY_END + +STEP 4 CHECK_ANSWER +ENTRY_BEGIN +MATCH opcode qname qtype +SECTION QUESTION +www.example.com. IN A +SECTION ANSWER +www.example.com. IN A 10.20.30.40 +ENTRY_END + +STEP 11 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +www2.example.com. IN A +ENTRY_END + +STEP 14 CHECK_ANSWER +ENTRY_BEGIN +MATCH opcode qname qtype +SECTION QUESTION +www2.example.com. IN A +SECTION ANSWER +www2.example.com. IN CNAME b.ex2.com. +b.ex2.com. IN A 10.20.30.42 +ENTRY_END + +STEP 21 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +www3.example.com. IN A +ENTRY_END + +STEP 24 CHECK_ANSWER +ENTRY_BEGIN +MATCH opcode qname qtype +SECTION QUESTION +www3.example.com. IN A +SECTION ANSWER +www3.example.com. IN CNAME b.ex2.com. +b.ex2.com. IN CNAME b.ex3.com. +b.ex3.com. IN A 10.20.30.43 +ENTRY_END + +STEP 31 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +www4.example.com. IN A +ENTRY_END + +STEP 34 CHECK_ANSWER +ENTRY_BEGIN +MATCH opcode qname qtype +SECTION QUESTION +www4.example.com. IN A +SECTION ANSWER +www4.example.com. IN CNAME b4.ex2.com. +b4.ex2.com. IN CNAME b4.ex3.com. +b4.ex3.com. IN CNAME b4.ex4.com. +b4.ex4.com. IN A 10.20.30.44 +ENTRY_END + +SCENARIO_END diff --git a/util/config_file.c b/util/config_file.c index ab209a818..6f44ab90b 100644 --- a/util/config_file.c +++ b/util/config_file.c @@ -252,6 +252,7 @@ config_create(void) cfg->harden_referral_path = 0; cfg->harden_algo_downgrade = 0; cfg->harden_unknown_additional = 0; + cfg->harden_cname_follow = 1; cfg->use_caps_bits_for_id = 0; cfg->caps_whitelist = NULL; cfg->private_address = NULL; @@ -737,6 +738,7 @@ int config_set_option(struct config_file* cfg, const char* opt, else S_YNO("harden-referral-path:", harden_referral_path) else S_YNO("harden-algo-downgrade:", harden_algo_downgrade) else S_YNO("harden-unknown-additional:", harden_unknown_additional) + else S_YNO("harden-cname-follow:", harden_cname_follow) else S_YNO("use-caps-for-id:", use_caps_bits_for_id) else S_STRLIST("caps-whitelist:", caps_whitelist) else S_SIZET_OR_ZERO("unwanted-reply-threshold:", unwanted_threshold) @@ -1240,6 +1242,7 @@ config_get_option(struct config_file* cfg, const char* opt, else O_YNO(opt, "harden-referral-path", harden_referral_path) else O_YNO(opt, "harden-algo-downgrade", harden_algo_downgrade) else O_YNO(opt, "harden-unknown-additional", harden_unknown_additional) + else O_YNO(opt, "harden-cname-follow", harden_cname_follow) else O_YNO(opt, "use-caps-for-id", use_caps_bits_for_id) else O_LST(opt, "caps-whitelist", caps_whitelist) else O_DEC(opt, "unwanted-reply-threshold", unwanted_threshold) diff --git a/util/config_file.h b/util/config_file.h index c2b45759f..819a321af 100644 --- a/util/config_file.h +++ b/util/config_file.h @@ -311,6 +311,8 @@ struct config_file { /** harden against unknown records in the authority section and in * the additional section */ int harden_unknown_additional; + /** harden to follow cname redirections */ + int harden_cname_follow; /** use 0x20 bits in query as random ID bits */ int use_caps_bits_for_id; /** 0x20 whitelist, domains that do not use capsforid */ diff --git a/util/configlexer.lex b/util/configlexer.lex index 196575bdb..5c477a44a 100644 --- a/util/configlexer.lex +++ b/util/configlexer.lex @@ -323,6 +323,7 @@ harden-below-nxdomain{COLON} { YDVAR(1, VAR_HARDEN_BELOW_NXDOMAIN) } harden-referral-path{COLON} { YDVAR(1, VAR_HARDEN_REFERRAL_PATH) } harden-algo-downgrade{COLON} { YDVAR(1, VAR_HARDEN_ALGO_DOWNGRADE) } harden-unknown-additional{COLON} { YDVAR(1, VAR_HARDEN_UNKNOWN_ADDITIONAL) } +harden-cname-follow{COLON} { YDVAR(1, VAR_HARDEN_CNAME_FOLLOW) } use-caps-for-id{COLON} { YDVAR(1, VAR_USE_CAPS_FOR_ID) } caps-whitelist{COLON} { YDVAR(1, VAR_CAPS_WHITELIST) } caps-exempt{COLON} { YDVAR(1, VAR_CAPS_WHITELIST) } diff --git a/util/configparser.y b/util/configparser.y index 490bba0ff..96c30de0c 100644 --- a/util/configparser.y +++ b/util/configparser.y @@ -219,6 +219,7 @@ extern struct config_parser_state* cfg_parser; %token VAR_MAX_TRANSFER_SIZE VAR_MAX_TRANSFER_TIME %token VAR_MAX_GLOBAL_QUOTA VAR_HARDEN_UNVERIFIED_GLUE VAR_LOG_TIME_ISO %token VAR_ITER_SCRUB_PROMISCUOUS VAR_LOG_THREAD_ID +%token VAR_HARDEN_CNAME_FOLLOW %% toplevelvars: /* empty */ | toplevelvars toplevelvar ; @@ -361,7 +362,8 @@ content_server: server_num_threads | server_verbosity | server_port | server_log_destaddr | server_cookie_secret_file | server_iter_scrub_ns | server_iter_scrub_cname | server_max_global_quota | server_iter_scrub_rrsig | - server_harden_unverified_glue | server_log_time_iso | server_iter_scrub_promiscuous + server_harden_unverified_glue | server_log_time_iso | server_iter_scrub_promiscuous | + server_harden_cname_follow ; stub_clause: stubstart contents_stub { @@ -1947,6 +1949,16 @@ server_harden_unknown_additional: VAR_HARDEN_UNKNOWN_ADDITIONAL STRING_ARG free($2); } ; +server_harden_cname_follow: VAR_HARDEN_CNAME_FOLLOW STRING_ARG + { + OUTYY(("P(server_harden_cname_follow:%s)\n", $2)); + if(strcmp($2, "yes") != 0 && strcmp($2, "no") != 0) + yyerror("expected yes or no."); + else cfg_parser->cfg->harden_cname_follow = + (strcmp($2, "yes")==0); + free($2); + } + ; server_use_caps_for_id: VAR_USE_CAPS_FOR_ID STRING_ARG { OUTYY(("P(server_use_caps_for_id:%s)\n", $2));