1 /*
2   Copyright 2020 Northern.tech AS
3 
4   This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5 
6   This program is free software; you can redistribute it and/or modify it
7   under the terms of the GNU General Public License as published by the
8   Free Software Foundation; version 3.
9 
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
18 
19   To the extent this program is licensed as part of the Enterprise
20   versions of CFEngine, the applicable Commercial Open Source License
21   (COSL) may apply to this file if you as a licensee so wish it. See
22   included file COSL.txt.
23 */
24 
25 #include <verify_vars.h>
26 
27 #include <actuator.h>
28 #include <attributes.h>
29 #include <regex.h>      /* CompileRegex,StringMatchFullWithPrecompiledRegex */
30 #include <buffer.h>
31 #include <misc_lib.h>
32 #include <fncall.h>
33 #include <rlist.h>
34 #include <conversion.h>
35 #include <expand.h>
36 #include <scope.h>
37 #include <promises.h>
38 #include <vars.h>
39 #include <matching.h>
40 #include <syntax.h>
41 #include <audit.h>
42 #include <string_lib.h>
43 #include <cleanup.h>
44 
45 typedef struct
46 {
47     bool should_converge;
48     bool ok_redefine;
49     bool drop_undefined;
50     Constraint *cp_save; // e.g. string => "foo"
51 } ConvergeVariableOptions;
52 
53 
54 static ConvergeVariableOptions CollectConvergeVariableOptions(EvalContext *ctx, const Promise *pp);
55 static bool Epimenides(EvalContext *ctx, const char *ns, const char *scope, const char *var, Rval rval, int level);
56 static bool CompareRval(const void *rval1_item, RvalType rval1_type, const void *rval2_item, RvalType rval2_type);
57 
IsLegalVariableName(EvalContext * ctx,const Promise * pp)58 static bool IsLegalVariableName(EvalContext *ctx, const Promise *pp)
59 {
60     const char *var_name = pp->promiser;
61 
62     /* TODO: remove at some point (global, leaked), but for now
63      * this offers an attractive speedup. */
64     static pcre *rx = NULL;
65     if (!rx)
66     {
67         /* \200-\377 is there for multibyte unicode characters */
68         rx = CompileRegex("[a-zA-Z0-9_\200-\377.]+(\\[.+\\])*"); /* Known leak, see TODO. */
69     }
70 
71     if (!StringMatchFullWithPrecompiledRegex(rx, var_name))
72     {
73         return false;
74     }
75 
76     char *bracket = strchr(var_name, '[');
77     char *dot = strchr(var_name, '.');
78     /* we only care about variable name prefix, not dots inside array keys */
79     if ((dot != NULL) && ((bracket == NULL) || (dot < bracket)))
80     {
81         /* detect and prevent remote bundle variable injection (CFE-1915) */
82         char *prefix = xstrndup(var_name, dot - var_name);
83         const Bundle *cur_bundle = PromiseGetBundle(pp);
84 
85         if (!StringEqual(prefix, cur_bundle->name))
86         {
87             Log(LOG_LEVEL_VERBOSE,
88                 "Variable '%s' may be attempted to be injected into a remote bundle",
89                 var_name);
90             if (StringSetContains(EvalContextGetBundleNames(ctx), prefix))
91             {
92                 Log(LOG_LEVEL_ERR, "Remote bundle variable injection detected!");
93                 free(prefix);
94                 return false;
95             }
96             /* the conflicting bundle may be defined later, we need to remember
97              * this promise as potentially dangerous */
98             EvalContextPushRemoteVarPromise(ctx, prefix, pp->org_pp);
99         }
100         free(prefix);
101     }
102 
103     return true;
104 }
105 
106 // TODO why not printing that new definition is skipped?
107 // TODO what with ifdefined?
108 
VerifyVarPromise(EvalContext * ctx,const Promise * pp,ARG_UNUSED void * param)109 PromiseResult VerifyVarPromise(EvalContext *ctx, const Promise *pp,
110                                ARG_UNUSED void *param)
111 {
112     ConvergeVariableOptions opts = CollectConvergeVariableOptions(ctx, pp);
113 
114     Log(LOG_LEVEL_DEBUG, "Evaluating vars promise: %s", pp->promiser);
115     LogDebug(LOG_MOD_VARS,
116              "ok_redefine=%d, drop_undefined=%d, should_converge=%d",
117              opts.ok_redefine, opts.drop_undefined, opts.should_converge);
118 
119     if (!opts.should_converge)
120     {
121         LogDebug(LOG_MOD_VARS,
122                  "Skipping vars promise because should_converge=false");
123         return PROMISE_RESULT_NOOP;
124     }
125 
126 //    opts.drop_undefined = true;         /* always remove @{unresolved_list} */
127 
128     Attributes a = ZeroAttributes;
129     // More consideration needs to be given to using these
130     //a.transaction = GetTransactionConstraints(pp);
131 
132     /* Warn if promise locking was used with a promise that doesn't support it
133      * (which applies to all of 'vars', 'meta' and 'defaults' promises handled
134      * by this code).
135      * (Only do this in the first pass in cf-promises, we don't have to repeat
136      * the warning over and over.) */
137     if (EvalContextGetPass(ctx) == 0 && THIS_AGENT_TYPE == AGENT_TYPE_COMMON)
138     {
139         int ifelapsed = PromiseGetConstraintAsInt(ctx, "ifelapsed", pp);
140         if (ifelapsed != CF_NOINT)
141         {
142             Log(LOG_LEVEL_WARNING,
143                 "ifelapsed attribute specified in action body for %s promise '%s',"
144                 " but %s promises do not support promise locking",
145                 pp->parent_section->promise_type, pp->promiser,
146                 pp->parent_section->promise_type);
147         }
148         int expireafter = PromiseGetConstraintAsInt(ctx, "expireafter", pp);
149         if (expireafter != CF_NOINT)
150         {
151             Log(LOG_LEVEL_WARNING,
152                 "expireafter attribute specified in action body for %s promise '%s',"
153                 " but %s promises do not support promise locking",
154                 pp->parent_section->promise_type, pp->promiser,
155                 pp->parent_section->promise_type);
156         }
157     }
158 
159     a.classes = GetClassDefinitionConstraints(ctx, pp);
160 
161     VarRef *ref = VarRefParseFromBundle(pp->promiser, PromiseGetBundle(pp));
162     if (strcmp("meta", pp->parent_section->promise_type) == 0)
163     {
164         VarRefSetMeta(ref, true);
165     }
166 
167     DataType existing_value_type = CF_DATA_TYPE_NONE;
168     const void *existing_value;
169     if (IsExpandable(pp->promiser))
170     {
171         existing_value = NULL;
172     }
173     else
174     {
175         existing_value = EvalContextVariableGet(ctx, ref, &existing_value_type);
176     }
177 
178     Rval rval = opts.cp_save->rval;
179     PromiseResult result;
180 
181     if (rval.item != NULL || rval.type == RVAL_TYPE_LIST)
182     {
183         DataType data_type = DataTypeFromString(opts.cp_save->lval);
184 
185         if (opts.cp_save->rval.type == RVAL_TYPE_FNCALL)
186         {
187             FnCall *fp = RvalFnCallValue(rval);
188             const FnCallType *fn = FnCallTypeGet(fp->name);
189             if (!fn)
190             {
191                 assert(false && "Canary: should have been caught before this point");
192                 FatalError(ctx, "While setting variable '%s' in bundle '%s', unknown function '%s'",
193                            pp->promiser, PromiseGetBundle(pp)->name, fp->name);
194             }
195 
196             if (fn->dtype != DataTypeFromString(opts.cp_save->lval))
197             {
198                 FatalError(ctx, "While setting variable '%s' in bundle '%s', variable declared type '%s' but function '%s' returns type '%s'",
199                            pp->promiser, PromiseGetBundle(pp)->name, opts.cp_save->lval,
200                            fp->name, DataTypeToString(fn->dtype));
201             }
202 
203             if (existing_value_type != CF_DATA_TYPE_NONE)
204             {
205                 // Already did this
206                 VarRefDestroy(ref);
207                 return PROMISE_RESULT_NOOP;
208             }
209 
210             FnCallResult res = FnCallEvaluate(ctx, PromiseGetPolicy(pp), fp, pp);
211 
212             if (res.status == FNCALL_FAILURE)
213             {
214                 /* We do not assign variables to failed fn calls */
215                 if (EvalContextGetPass(ctx) == CF_DONEPASSES-1) {
216                     // If we still fail at last pass, make a log
217                     Log(LOG_LEVEL_VERBOSE, "While setting variable '%s' in bundle '%s', function '%s' failed - skipping",
218                                        pp->promiser, PromiseGetBundle(pp)->name, fp->name);
219                 }
220                 RvalDestroy(res.rval);
221                 VarRefDestroy(ref);
222                 return PROMISE_RESULT_NOOP;
223             }
224             else
225             {
226                 rval = res.rval;
227             }
228         }
229         else
230         {
231             Buffer *conv = BufferNew();
232             bool malformed = false, misprint = false;
233 
234             if (strcmp(opts.cp_save->lval, "int") == 0)
235             {
236                 long int asint = IntFromString(opts.cp_save->rval.item);
237                 if (asint == CF_NOINT)
238                 {
239                     malformed = true;
240                 }
241                 else if (0 > BufferPrintf(conv, "%ld", asint))
242                 {
243                     misprint = true;
244                 }
245                 else
246                 {
247                     rval = RvalNew(BufferData(conv), opts.cp_save->rval.type);
248                 }
249             }
250             else if (strcmp(opts.cp_save->lval, "real") == 0)
251             {
252                 double real_value;
253                 if (!DoubleFromString(opts.cp_save->rval.item, &real_value))
254                 {
255                     malformed = true;
256                 }
257                 else if (0 > BufferPrintf(conv, "%lf", real_value))
258                 {
259                     misprint = true;
260                 }
261                 else
262                 {
263                     rval = RvalNew(BufferData(conv), opts.cp_save->rval.type);
264                 }
265             }
266             else
267             {
268                 rval = RvalCopy(opts.cp_save->rval);
269             }
270             BufferDestroy(conv);
271 
272             if (malformed)
273             {
274                 /* Arises when opts->cp_save->rval.item isn't yet expanded. */
275                 /* Has already been logged by *FromString */
276                 VarRefDestroy(ref);
277                 return PROMISE_RESULT_FAIL;
278             }
279             else if (misprint)
280             {
281                 /* Even though no problems with memory allocation can
282                  * get here, there might be other problems. */
283                 UnexpectedError("Problems writing to buffer");
284                 VarRefDestroy(ref);
285                 return PROMISE_RESULT_FAIL;
286             }
287             else if (rval.type == RVAL_TYPE_LIST)
288             {
289                 Rlist *rval_list = RvalRlistValue(rval);
290                 RlistFlatten(ctx, &rval_list);
291                 rval.item = rval_list;
292             }
293         }
294 
295         if (Epimenides(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name, pp->promiser, rval, 0))
296         {
297             Log(LOG_LEVEL_ERR, "Variable '%s' contains itself indirectly - an unkeepable promise", pp->promiser);
298             DoCleanupAndExit(EXIT_FAILURE);
299         }
300         else
301         {
302             /* See if the variable needs recursively expanding again */
303 
304             Rval returnval = EvaluateFinalRval(ctx, PromiseGetPolicy(pp), ref->ns, ref->scope, rval, true, pp);
305 
306             RvalDestroy(rval);
307 
308             // freed before function exit
309             rval = returnval;
310         }
311 
312         /* If variable did resolve but we're not allowed to modify it. */
313         /* ok_redefine: only on second iteration, else we ignore broken promises. TODO wat? */
314         if (existing_value_type != CF_DATA_TYPE_NONE &&
315             !opts.ok_redefine)
316         {
317             if (!CompareRval(existing_value, DataTypeToRvalType(existing_value_type),
318                              rval.item, rval.type))
319             {
320                 switch (rval.type)
321                 {
322                     /* TODO redefinition shouldn't be mentioned. Maybe handle like normal variable definition/ */
323                 case RVAL_TYPE_SCALAR:
324                     Log(LOG_LEVEL_VERBOSE, "V: Skipping redefinition of constant scalar '%s': from '%s' to '%s'",
325                         pp->promiser, (const char *)existing_value, RvalScalarValue(rval));
326                     PromiseRef(LOG_LEVEL_VERBOSE, pp);
327                     break;
328 
329                 case RVAL_TYPE_LIST:
330                 {
331                     Log(LOG_LEVEL_VERBOSE, "V: Skipping redefinition of constant list '%s'", pp->promiser);
332                     Writer *w = StringWriter();
333                     RlistWrite(w, existing_value);
334                     char *oldstr = StringWriterClose(w);
335                     Log(LOG_LEVEL_DEBUG, "Old value:         %s", oldstr);
336                     free(oldstr);
337 
338                     w = StringWriter();
339                     RlistWrite(w, rval.item);
340                     char *newstr = StringWriterClose(w);
341                     Log(LOG_LEVEL_DEBUG, "Skipped new value: %s", newstr);
342                     free(newstr);
343 
344                     PromiseRef(LOG_LEVEL_VERBOSE, pp);
345                 }
346                 break;
347 
348                 case RVAL_TYPE_CONTAINER:
349                 case RVAL_TYPE_FNCALL:
350                 case RVAL_TYPE_NOPROMISEE:
351                     break;
352                 }
353             }
354 
355             RvalDestroy(rval);
356             VarRefDestroy(ref);
357             return PROMISE_RESULT_NOOP;
358         }
359 
360         if (IsCf3VarString(pp->promiser))
361         {
362             // Unexpanded variables, we don't do anything with
363             RvalDestroy(rval);
364             VarRefDestroy(ref);
365             return PROMISE_RESULT_NOOP;
366         }
367 
368         if (!IsLegalVariableName(ctx, pp))
369         {
370             Log(LOG_LEVEL_ERR, "Variable identifier '%s' is not legal", pp->promiser);
371             PromiseRef(LOG_LEVEL_ERR, pp);
372             RvalDestroy(rval);
373             VarRefDestroy(ref);
374             return PROMISE_RESULT_NOOP;
375         }
376 
377         if (rval.type == RVAL_TYPE_LIST)
378         {
379             if (opts.drop_undefined)
380             {
381                 Rlist *stripped = RvalRlistValue(rval);
382                 Rlist *entry = stripped;
383                 while (entry)
384                 {
385                     Rlist *delete_me = NULL;
386                     if (IsNakedVar(RlistScalarValue(entry), '@'))
387                     {
388                         delete_me = entry;
389                     }
390                     entry = entry->next;
391                     RlistDestroyEntry(&stripped, delete_me);
392                 }
393                 rval.item = stripped;
394             }
395 
396             for (const Rlist *rp = RvalRlistValue(rval); rp; rp = rp->next)
397             {
398                 if (rp->val.type != RVAL_TYPE_SCALAR)
399                 {
400                     // Cannot assign variable because value is a list containing a non-scalar item
401                     VarRefDestroy(ref);
402                     RvalDestroy(rval);
403                     return PROMISE_RESULT_NOOP;
404                 }
405             }
406         }
407 
408         if (ref->num_indices > 0)
409         {
410             if (data_type == CF_DATA_TYPE_CONTAINER)
411             {
412                 char *lval_str = VarRefToString(ref, true);
413                 Log(LOG_LEVEL_ERR, "Cannot assign a container to an indexed variable name '%s'. Should be assigned to '%s' instead",
414                     lval_str, ref->lval);
415                 free(lval_str);
416                 VarRefDestroy(ref);
417                 RvalDestroy(rval);
418                 return PROMISE_RESULT_NOOP;
419             }
420             else
421             {
422                 DataType existing_type;
423                 VarRef *base_ref = VarRefCopyIndexless(ref);
424                 if (EvalContextVariableGet(ctx, ref, &existing_type) && existing_type == CF_DATA_TYPE_CONTAINER)
425                 {
426                     char *lval_str = VarRefToString(ref, true);
427                     char *base_ref_str = VarRefToString(base_ref, true);
428                     Log(LOG_LEVEL_ERR, "Cannot assign value to indexed variable name '%s', because a container is already assigned to the base name '%s'",
429                         lval_str, base_ref_str);
430                     free(lval_str);
431                     free(base_ref_str);
432                     VarRefDestroy(base_ref);
433                     VarRefDestroy(ref);
434                     RvalDestroy(rval);
435                     return PROMISE_RESULT_NOOP;
436                 }
437                 VarRefDestroy(base_ref);
438             }
439         }
440 
441 
442         DataType required_datatype = DataTypeFromString(opts.cp_save->lval);
443         if (rval.type != DataTypeToRvalType(required_datatype))
444         {
445             char *ref_str = VarRefToString(ref, true);
446             char *value_str = RvalToString(rval);
447             Log(LOG_LEVEL_ERR, "Variable '%s' expected a variable of type '%s', but was given incompatible value '%s'",
448                 ref_str, DataTypeToString(required_datatype), value_str);
449             PromiseRef(LOG_LEVEL_ERR, pp);
450 
451             free(ref_str);
452             free(value_str);
453             VarRefDestroy(ref);
454             RvalDestroy(rval);
455             return PROMISE_RESULT_FAIL;
456         }
457 
458         /* WRITE THE VARIABLE AT LAST. */
459         bool success = EvalContextVariablePut(ctx, ref, rval.item, required_datatype, "source=promise");
460 
461         if (!success)
462         {
463             Log(LOG_LEVEL_VERBOSE,
464                 "Unable to converge %s.%s value (possibly empty or infinite regression)",
465                 ref->scope, pp->promiser);
466             PromiseRef(LOG_LEVEL_VERBOSE, pp);
467 
468             VarRefDestroy(ref);
469             RvalDestroy(rval);
470             return PROMISE_RESULT_FAIL;
471         }
472 
473         Rlist *promise_meta = PromiseGetConstraintAsList(ctx, "meta", pp);
474         if (promise_meta)
475         {
476             StringSet *class_meta = EvalContextVariableTags(ctx, ref);
477             Buffer *print;
478             for (const Rlist *rp = promise_meta; rp; rp = rp->next)
479             {
480                 StringSetAdd(class_meta, xstrdup(RlistScalarValue(rp)));
481                 print = StringSetToBuffer(class_meta, ',');
482                 Log(LOG_LEVEL_DEBUG,
483                     "Added tag %s to class %s, tags now [%s]",
484                     RlistScalarValue(rp), pp->promiser, BufferData(print));
485                 BufferDestroy(print);
486             }
487         }
488 
489         result = PROMISE_RESULT_NOOP;
490     }
491     else
492     {
493         Log(LOG_LEVEL_ERR, "Variable %s has no promised value", pp->promiser);
494         Log(LOG_LEVEL_ERR, "Rule from %s at/before line %zu", PromiseGetBundle(pp)->source_path, opts.cp_save->offset.line);
495         result = PROMISE_RESULT_FAIL;
496     }
497 
498     /*
499      * FIXME: Variable promise are exempt from normal evaluation logic still, so
500      * they are not pushed to evaluation stack before being evaluated. Due to
501      * this reason, we cannot call cfPS here to set classes, as it will error
502      * out with ProgrammingError.
503      *
504      * In order to support 'classes' body for variables as well, we call
505      * ClassAuditLog explicitly.
506      */
507     ClassAuditLog(ctx, pp, &a, result);
508 
509     VarRefDestroy(ref);
510     RvalDestroy(rval);
511 
512     return result;
513 }
514 
CompareRval(const void * rval1_item,RvalType rval1_type,const void * rval2_item,RvalType rval2_type)515 static bool CompareRval(const void *rval1_item, RvalType rval1_type,
516                         const void *rval2_item, RvalType rval2_type)
517 {
518     if (rval1_type != rval2_type)
519     {
520         return false;
521     }
522 
523     switch (rval1_type)
524     {
525     case RVAL_TYPE_SCALAR:
526 
527         if (IsCf3VarString(rval1_item) || IsCf3VarString(rval2_item))
528         {
529             return false;          // inconclusive
530         }
531 
532         if (strcmp(rval1_item, rval2_item) != 0)
533         {
534             return false;
535         }
536 
537         break;
538 
539     case RVAL_TYPE_LIST:
540         return RlistEqual(rval1_item, rval2_item);
541 
542     case RVAL_TYPE_FNCALL:
543         return false;
544 
545     default:
546         return false;
547     }
548 
549     return true;
550 }
551 
Epimenides(EvalContext * ctx,const char * ns,const char * scope,const char * var,Rval rval,int level)552 static bool Epimenides(EvalContext *ctx, const char *ns, const char *scope, const char *var, Rval rval, int level)
553 {
554     switch (rval.type)
555     {
556     case RVAL_TYPE_SCALAR:
557 
558         if (StringContainsVar(RvalScalarValue(rval), var))
559         {
560             Log(LOG_LEVEL_ERR, "Scalar variable '%s' contains itself (non-convergent) '%s'", var, RvalScalarValue(rval));
561             return true;
562         }
563 
564         if (IsCf3VarString(RvalScalarValue(rval)))
565         {
566             Buffer *exp = BufferNew();
567             ExpandScalar(ctx, ns, scope, RvalScalarValue(rval), exp);
568 
569             if (strcmp(BufferData(exp), RvalScalarValue(rval)) == 0)
570             {
571                 BufferDestroy(exp);
572                 return false;
573             }
574 
575             if (level > 3)
576             {
577                 BufferDestroy(exp);
578                 return false;
579             }
580 
581             if (Epimenides(ctx, ns, scope, var, (Rval) { BufferGet(exp), RVAL_TYPE_SCALAR}, level + 1))
582             {
583                 BufferDestroy(exp);
584                 return true;
585             }
586 
587             BufferDestroy(exp);
588         }
589 
590         break;
591 
592     case RVAL_TYPE_LIST:
593         for (const Rlist *rp = RvalRlistValue(rval); rp != NULL; rp = rp->next)
594         {
595             if (Epimenides(ctx, ns, scope, var, rp->val, level))
596             {
597                 return true;
598             }
599         }
600         break;
601 
602     case RVAL_TYPE_CONTAINER:
603     case RVAL_TYPE_FNCALL:
604     case RVAL_TYPE_NOPROMISEE:
605         return false;
606     }
607 
608     return false;
609 }
610 
611 /**
612  * @brief Collects variable constraints controlling how the promise should be converged
613  */
CollectConvergeVariableOptions(EvalContext * ctx,const Promise * pp)614 static ConvergeVariableOptions CollectConvergeVariableOptions(EvalContext *ctx, const Promise *pp)
615 {
616     ConvergeVariableOptions opts;
617     opts.drop_undefined = false;
618     opts.cp_save = NULL;                             /* main variable value */
619     /* By default allow variable redefinition, use "policy" constraint
620      * to override. */
621     opts.ok_redefine = true;
622     /* Main return value: becomes true at the end of the function. */
623     opts.should_converge = false;
624 
625     if (!IsDefinedClass(ctx, pp->classes))
626     {
627         return opts;
628     }
629 
630     int num_values = 0;
631     for (size_t i = 0; i < SeqLength(pp->conlist); i++)
632     {
633         Constraint *cp = SeqAt(pp->conlist, i);
634 
635         if (strcmp(cp->lval, "comment") == 0)
636         {
637             // Comments don't affect convergence
638             // Unclear why this is in the constraint list in the first place?
639             continue;
640         }
641         else if (cp->rval.item == NULL && cp->rval.type != RVAL_TYPE_LIST)
642         {
643             // No right value, considered empty
644             continue;
645         }
646         else if (strcmp(cp->lval, "ifvarclass") == 0 ||
647                  strcmp(cp->lval, "if")         == 0)
648         {
649             switch (cp->rval.type)
650             {
651             case RVAL_TYPE_SCALAR:
652                 if (!IsDefinedClass(ctx, cp->rval.item))
653                 {
654                     return opts;
655                 }
656 
657                 break;
658 
659             case RVAL_TYPE_FNCALL:
660             {
661                 bool excluded = false;
662 
663                 /* eval it: e.g. ifvarclass => not("a_class") */
664 
665                 Rval res = FnCallEvaluate(ctx, PromiseGetPolicy(pp), cp->rval.item, pp).rval;
666 
667                 /* Don't continue unless function was evaluated properly */
668                 if (res.type != RVAL_TYPE_SCALAR)
669                 {
670                     RvalDestroy(res);
671                     return opts;
672                 }
673 
674                 excluded = !IsDefinedClass(ctx, res.item);
675 
676                 RvalDestroy(res);
677 
678                 if (excluded)
679                 {
680                     return opts;
681                 }
682             }
683             break;
684 
685             default:
686                 Log(LOG_LEVEL_ERR, "Invalid if/ifvarclass type '%c': should be string or function", cp->rval.type);
687             }
688         }
689         else if (strcmp(cp->lval, "policy") == 0)
690         {
691             if (strcmp(cp->rval.item, "ifdefined") == 0)
692             {
693                 opts.drop_undefined = true;
694             }
695             else if (strcmp(cp->rval.item, "constant") == 0)
696             {
697                 opts.ok_redefine = false;
698             }
699         }
700         else if (DataTypeFromString(cp->lval) != CF_DATA_TYPE_NONE)
701         {
702             num_values++;
703             opts.cp_save = cp;
704         }
705     }
706 
707     if (opts.cp_save == NULL)
708     {
709         Log(LOG_LEVEL_WARNING, "Incomplete vars promise: %s",
710             pp->promiser);
711         PromiseRef(LOG_LEVEL_INFO, pp);
712         return opts;
713     }
714 
715     if (num_values > 2)
716     {
717         Log(LOG_LEVEL_ERR,
718             "Variable '%s' breaks its own promise with multiple (%d) values",
719             pp->promiser, num_values);
720         PromiseRef(LOG_LEVEL_ERR, pp);
721         return opts;
722     }
723 
724     /* All constraints look OK, and classes are defined. Move forward with
725      * this promise. */
726     opts.should_converge = true;
727 
728     return opts;
729 }
730