1 /*
2   Copyright 2021 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     assert(pp != NULL);
113 
114     ConvergeVariableOptions opts = CollectConvergeVariableOptions(ctx, pp);
115 
116     Log(LOG_LEVEL_DEBUG, "Evaluating vars promise: %s", pp->promiser);
117     LogDebug(LOG_MOD_VARS,
118              "ok_redefine=%d, drop_undefined=%d, should_converge=%d",
119              opts.ok_redefine, opts.drop_undefined, opts.should_converge);
120 
121     if (!opts.should_converge)
122     {
123         LogDebug(LOG_MOD_VARS,
124                  "Skipping vars promise because should_converge=false");
125         return PROMISE_RESULT_NOOP;
126     }
127 
128 //    opts.drop_undefined = true;         /* always remove @{unresolved_list} */
129 
130     Attributes a = ZeroAttributes;
131     // More consideration needs to be given to using these
132     //a.transaction = GetTransactionConstraints(pp);
133 
134     /* Warn if promise locking was used with a promise that doesn't support it
135      * (which applies to all of 'vars', 'meta' and 'defaults' promises handled
136      * by this code).
137      * 'ifelapsed => "0"' (e.g. with 'action => immediate') can however be used
138      * to make sure cached functions are called every time. [ENT-7478]
139      * (Only do this in the first pass in cf-promises, we don't have to repeat
140      * the warning over and over.) */
141     if (EvalContextGetPass(ctx) == 0 && THIS_AGENT_TYPE == AGENT_TYPE_COMMON)
142     {
143         int ifelapsed = PromiseGetConstraintAsInt(ctx, "ifelapsed", pp);
144         if ((ifelapsed != CF_NOINT) &&
145             ((ifelapsed != 0) || !StringEqual(PromiseGetPromiseType(pp), "vars")))
146         {
147             Log(LOG_LEVEL_WARNING,
148                 "ifelapsed attribute specified in action body for %s promise '%s',"
149                 " but %s promises do not support promise locking",
150                 PromiseGetPromiseType(pp), pp->promiser,
151                 PromiseGetPromiseType(pp));
152         }
153         int expireafter = PromiseGetConstraintAsInt(ctx, "expireafter", pp);
154         if (expireafter != CF_NOINT)
155         {
156             Log(LOG_LEVEL_WARNING,
157                 "expireafter attribute specified in action body for %s promise '%s',"
158                 " but %s promises do not support promise locking",
159                 PromiseGetPromiseType(pp), pp->promiser,
160                 PromiseGetPromiseType(pp));
161         }
162     }
163 
164     a.classes = GetClassDefinitionConstraints(ctx, pp);
165 
166     VarRef *ref = VarRefParseFromBundle(pp->promiser, PromiseGetBundle(pp));
167     if (strcmp("meta", PromiseGetPromiseType(pp)) == 0)
168     {
169         VarRefSetMeta(ref, true);
170     }
171 
172     DataType existing_value_type = CF_DATA_TYPE_NONE;
173     const void *existing_value;
174     if (IsExpandable(pp->promiser))
175     {
176         existing_value = NULL;
177     }
178     else
179     {
180         existing_value = EvalContextVariableGet(ctx, ref, &existing_value_type);
181     }
182 
183     Rval rval = opts.cp_save->rval;
184     PromiseResult result;
185 
186     if (rval.item != NULL || rval.type == RVAL_TYPE_LIST)
187     {
188         DataType data_type = DataTypeFromString(opts.cp_save->lval);
189 
190         if (opts.cp_save->rval.type == RVAL_TYPE_FNCALL)
191         {
192             FnCall *fp = RvalFnCallValue(rval);
193             const FnCallType *fn = FnCallTypeGet(fp->name);
194             if (!fn)
195             {
196                 assert(false && "Canary: should have been caught before this point");
197                 FatalError(ctx, "While setting variable '%s' in bundle '%s', unknown function '%s'",
198                            pp->promiser, PromiseGetBundle(pp)->name, fp->name);
199             }
200 
201             if (fn->dtype != DataTypeFromString(opts.cp_save->lval))
202             {
203                 FatalError(ctx, "While setting variable '%s' in bundle '%s', variable declared type '%s' but function '%s' returns type '%s'",
204                            pp->promiser, PromiseGetBundle(pp)->name, opts.cp_save->lval,
205                            fp->name, DataTypeToString(fn->dtype));
206             }
207 
208             if (existing_value_type != CF_DATA_TYPE_NONE)
209             {
210                 // Already did this
211                 VarRefDestroy(ref);
212                 return PROMISE_RESULT_NOOP;
213             }
214 
215             FnCallResult res = FnCallEvaluate(ctx, PromiseGetPolicy(pp), fp, pp);
216 
217             if (res.status == FNCALL_FAILURE)
218             {
219                 /* We do not assign variables to failed fn calls */
220                 if (EvalContextGetPass(ctx) == CF_DONEPASSES-1) {
221                     // If we still fail at last pass, make a log
222                     Log(LOG_LEVEL_VERBOSE, "While setting variable '%s' in bundle '%s', function '%s' failed - skipping",
223                                        pp->promiser, PromiseGetBundle(pp)->name, fp->name);
224                 }
225                 RvalDestroy(res.rval);
226                 VarRefDestroy(ref);
227                 return PROMISE_RESULT_NOOP;
228             }
229             else
230             {
231                 rval = res.rval;
232             }
233         }
234         else
235         {
236             Buffer *conv = BufferNew();
237             bool malformed = false, misprint = false;
238 
239             if (strcmp(opts.cp_save->lval, "int") == 0)
240             {
241                 long int asint = IntFromString(opts.cp_save->rval.item);
242                 if (asint == CF_NOINT)
243                 {
244                     malformed = true;
245                 }
246                 else if (0 > BufferPrintf(conv, "%ld", asint))
247                 {
248                     misprint = true;
249                 }
250                 else
251                 {
252                     rval = RvalNew(BufferData(conv), opts.cp_save->rval.type);
253                 }
254             }
255             else if (strcmp(opts.cp_save->lval, "real") == 0)
256             {
257                 double real_value;
258                 if (!DoubleFromString(opts.cp_save->rval.item, &real_value))
259                 {
260                     malformed = true;
261                 }
262                 else if (0 > BufferPrintf(conv, "%lf", real_value))
263                 {
264                     misprint = true;
265                 }
266                 else
267                 {
268                     rval = RvalNew(BufferData(conv), opts.cp_save->rval.type);
269                 }
270             }
271             else
272             {
273                 rval = RvalCopy(opts.cp_save->rval);
274             }
275             BufferDestroy(conv);
276 
277             if (malformed)
278             {
279                 /* Arises when opts->cp_save->rval.item isn't yet expanded. */
280                 /* Has already been logged by *FromString */
281                 VarRefDestroy(ref);
282                 return PROMISE_RESULT_FAIL;
283             }
284             else if (misprint)
285             {
286                 /* Even though no problems with memory allocation can
287                  * get here, there might be other problems. */
288                 UnexpectedError("Problems writing to buffer");
289                 VarRefDestroy(ref);
290                 return PROMISE_RESULT_FAIL;
291             }
292             else if (rval.type == RVAL_TYPE_LIST)
293             {
294                 Rlist *rval_list = RvalRlistValue(rval);
295                 RlistFlatten(ctx, &rval_list);
296                 rval.item = rval_list;
297             }
298         }
299 
300         if (Epimenides(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name, pp->promiser, rval, 0))
301         {
302             Log(LOG_LEVEL_ERR, "Variable '%s' contains itself indirectly - an unkeepable promise", pp->promiser);
303             DoCleanupAndExit(EXIT_FAILURE);
304         }
305         else
306         {
307             /* See if the variable needs recursively expanding again */
308 
309             Rval returnval = EvaluateFinalRval(ctx, PromiseGetPolicy(pp), ref->ns, ref->scope, rval, true, pp);
310 
311             RvalDestroy(rval);
312 
313             // freed before function exit
314             rval = returnval;
315         }
316 
317         /* If variable did resolve but we're not allowed to modify it. */
318         /* ok_redefine: only on second iteration, else we ignore broken promises. TODO wat? */
319         if (existing_value_type != CF_DATA_TYPE_NONE &&
320             !opts.ok_redefine)
321         {
322             if (!CompareRval(existing_value, DataTypeToRvalType(existing_value_type),
323                              rval.item, rval.type))
324             {
325                 switch (rval.type)
326                 {
327                     /* TODO redefinition shouldn't be mentioned. Maybe handle like normal variable definition/ */
328                 case RVAL_TYPE_SCALAR:
329                     Log(LOG_LEVEL_VERBOSE, "V: Skipping redefinition of constant scalar '%s': from '%s' to '%s'",
330                         pp->promiser, (const char *)existing_value, RvalScalarValue(rval));
331                     PromiseRef(LOG_LEVEL_VERBOSE, pp);
332                     break;
333 
334                 case RVAL_TYPE_LIST:
335                 {
336                     Log(LOG_LEVEL_VERBOSE, "V: Skipping redefinition of constant list '%s'", pp->promiser);
337                     Writer *w = StringWriter();
338                     RlistWrite(w, existing_value);
339                     char *oldstr = StringWriterClose(w);
340                     Log(LOG_LEVEL_DEBUG, "Old value:         %s", oldstr);
341                     free(oldstr);
342 
343                     w = StringWriter();
344                     RlistWrite(w, rval.item);
345                     char *newstr = StringWriterClose(w);
346                     Log(LOG_LEVEL_DEBUG, "Skipped new value: %s", newstr);
347                     free(newstr);
348 
349                     PromiseRef(LOG_LEVEL_VERBOSE, pp);
350                 }
351                 break;
352 
353                 case RVAL_TYPE_CONTAINER:
354                 case RVAL_TYPE_FNCALL:
355                 case RVAL_TYPE_NOPROMISEE:
356                     break;
357                 }
358             }
359 
360             RvalDestroy(rval);
361             VarRefDestroy(ref);
362             return PROMISE_RESULT_NOOP;
363         }
364 
365         if (IsCf3VarString(pp->promiser))
366         {
367             // Unexpanded variables, we don't do anything with
368             RvalDestroy(rval);
369             VarRefDestroy(ref);
370             return PROMISE_RESULT_NOOP;
371         }
372 
373         if (!IsLegalVariableName(ctx, pp))
374         {
375             Log(LOG_LEVEL_ERR, "Variable identifier '%s' is not legal", pp->promiser);
376             PromiseRef(LOG_LEVEL_ERR, pp);
377             RvalDestroy(rval);
378             VarRefDestroy(ref);
379             return PROMISE_RESULT_NOOP;
380         }
381 
382         if (rval.type == RVAL_TYPE_LIST)
383         {
384             if (opts.drop_undefined)
385             {
386                 Rlist *stripped = RvalRlistValue(rval);
387                 Rlist *entry = stripped;
388                 while (entry)
389                 {
390                     Rlist *delete_me = NULL;
391                     if (IsNakedVar(RlistScalarValue(entry), '@'))
392                     {
393                         delete_me = entry;
394                     }
395                     entry = entry->next;
396                     RlistDestroyEntry(&stripped, delete_me);
397                 }
398                 rval.item = stripped;
399             }
400 
401             for (const Rlist *rp = RvalRlistValue(rval); rp; rp = rp->next)
402             {
403                 if (rp->val.type != RVAL_TYPE_SCALAR)
404                 {
405                     // Cannot assign variable because value is a list containing a non-scalar item
406                     VarRefDestroy(ref);
407                     RvalDestroy(rval);
408                     return PROMISE_RESULT_NOOP;
409                 }
410             }
411         }
412 
413         if (ref->num_indices > 0)
414         {
415             if (data_type == CF_DATA_TYPE_CONTAINER)
416             {
417                 char *lval_str = VarRefToString(ref, true);
418                 Log(LOG_LEVEL_ERR, "Cannot assign a container to an indexed variable name '%s'. Should be assigned to '%s' instead",
419                     lval_str, ref->lval);
420                 free(lval_str);
421                 VarRefDestroy(ref);
422                 RvalDestroy(rval);
423                 return PROMISE_RESULT_NOOP;
424             }
425             else
426             {
427                 DataType existing_type;
428                 VarRef *base_ref = VarRefCopyIndexless(ref);
429                 if (EvalContextVariableGet(ctx, ref, &existing_type) && existing_type == CF_DATA_TYPE_CONTAINER)
430                 {
431                     char *lval_str = VarRefToString(ref, true);
432                     char *base_ref_str = VarRefToString(base_ref, true);
433                     Log(LOG_LEVEL_ERR, "Cannot assign value to indexed variable name '%s', because a container is already assigned to the base name '%s'",
434                         lval_str, base_ref_str);
435                     free(lval_str);
436                     free(base_ref_str);
437                     VarRefDestroy(base_ref);
438                     VarRefDestroy(ref);
439                     RvalDestroy(rval);
440                     return PROMISE_RESULT_NOOP;
441                 }
442                 VarRefDestroy(base_ref);
443             }
444         }
445 
446 
447         DataType required_datatype = DataTypeFromString(opts.cp_save->lval);
448         if (rval.type != DataTypeToRvalType(required_datatype))
449         {
450             char *ref_str = VarRefToString(ref, true);
451             char *value_str = RvalToString(rval);
452             Log(LOG_LEVEL_ERR, "Variable '%s' expected a variable of type '%s', but was given incompatible value '%s'",
453                 ref_str, DataTypeToString(required_datatype), value_str);
454             PromiseRef(LOG_LEVEL_ERR, pp);
455 
456             free(ref_str);
457             free(value_str);
458             VarRefDestroy(ref);
459             RvalDestroy(rval);
460             return PROMISE_RESULT_FAIL;
461         }
462 
463         StringSet *tags = StringSetNew();
464         StringSetAdd(tags, xstrdup("source=promise"));
465 
466         Rlist *promise_meta = PromiseGetConstraintAsList(ctx, "meta", pp);
467         if (promise_meta)
468         {
469             Buffer *print;
470             for (const Rlist *rp = promise_meta; rp; rp = rp->next)
471             {
472                 StringSetAdd(tags, xstrdup(RlistScalarValue(rp)));
473                 if (WouldLog(LOG_LEVEL_DEBUG))
474                 {
475                     print = StringSetToBuffer(tags, ',');
476                     Log(LOG_LEVEL_DEBUG,
477                         "Added tag %s to variable %s tags (now [%s])",
478                         RlistScalarValue(rp), pp->promiser, BufferData(print));
479                     BufferDestroy(print);
480                 }
481             }
482         }
483 
484         const char *comment = PromiseGetConstraintAsRval(pp, "comment", RVAL_TYPE_SCALAR);
485 
486         /* WRITE THE VARIABLE AT LAST. */
487         bool success = EvalContextVariablePutTagsSetWithComment(ctx, ref, rval.item, required_datatype,
488                                                                 tags, comment);
489         if (success && (comment != NULL))
490         {
491             Log(LOG_LEVEL_VERBOSE, "Added variable '%s' with comment '%s'",
492                 pp->promiser, comment);
493         }
494 
495         if (!success)
496         {
497             Log(LOG_LEVEL_VERBOSE,
498                 "Unable to converge %s.%s value (possibly empty or infinite regression)",
499                 ref->scope, pp->promiser);
500             PromiseRef(LOG_LEVEL_VERBOSE, pp);
501 
502             VarRefDestroy(ref);
503             RvalDestroy(rval);
504             StringSetDestroy(tags);
505             return PROMISE_RESULT_FAIL;
506         }
507 
508         result = PROMISE_RESULT_NOOP;
509     }
510     else
511     {
512         Log(LOG_LEVEL_ERR, "Variable %s has no promised value", pp->promiser);
513         Log(LOG_LEVEL_ERR, "Rule from %s at/before line %zu", PromiseGetBundle(pp)->source_path, opts.cp_save->offset.line);
514         result = PROMISE_RESULT_FAIL;
515     }
516 
517     /*
518      * FIXME: Variable promise are exempt from normal evaluation logic still, so
519      * they are not pushed to evaluation stack before being evaluated. Due to
520      * this reason, we cannot call cfPS here to set classes, as it will error
521      * out with ProgrammingError.
522      *
523      * In order to support 'classes' body for variables as well, we call
524      * ClassAuditLog explicitly.
525      */
526     ClassAuditLog(ctx, pp, &a, result);
527 
528     VarRefDestroy(ref);
529     RvalDestroy(rval);
530 
531     return result;
532 }
533 
CompareRval(const void * rval1_item,RvalType rval1_type,const void * rval2_item,RvalType rval2_type)534 static bool CompareRval(const void *rval1_item, RvalType rval1_type,
535                         const void *rval2_item, RvalType rval2_type)
536 {
537     if (rval1_type != rval2_type)
538     {
539         return false;
540     }
541 
542     switch (rval1_type)
543     {
544     case RVAL_TYPE_SCALAR:
545 
546         if (IsCf3VarString(rval1_item) || IsCf3VarString(rval2_item))
547         {
548             return false;          // inconclusive
549         }
550 
551         if (strcmp(rval1_item, rval2_item) != 0)
552         {
553             return false;
554         }
555 
556         break;
557 
558     case RVAL_TYPE_LIST:
559         return RlistEqual(rval1_item, rval2_item);
560 
561     case RVAL_TYPE_FNCALL:
562         return false;
563 
564     default:
565         return false;
566     }
567 
568     return true;
569 }
570 
Epimenides(EvalContext * ctx,const char * ns,const char * scope,const char * var,Rval rval,int level)571 static bool Epimenides(EvalContext *ctx, const char *ns, const char *scope, const char *var, Rval rval, int level)
572 {
573     switch (rval.type)
574     {
575     case RVAL_TYPE_SCALAR:
576 
577         if (StringContainsVar(RvalScalarValue(rval), var))
578         {
579             Log(LOG_LEVEL_ERR, "Scalar variable '%s' contains itself (non-convergent) '%s'", var, RvalScalarValue(rval));
580             return true;
581         }
582 
583         if (IsCf3VarString(RvalScalarValue(rval)))
584         {
585             Buffer *exp = BufferNew();
586             ExpandScalar(ctx, ns, scope, RvalScalarValue(rval), exp);
587 
588             if (strcmp(BufferData(exp), RvalScalarValue(rval)) == 0)
589             {
590                 BufferDestroy(exp);
591                 return false;
592             }
593 
594             if (level > 3)
595             {
596                 BufferDestroy(exp);
597                 return false;
598             }
599 
600             if (Epimenides(ctx, ns, scope, var, (Rval) { BufferGet(exp), RVAL_TYPE_SCALAR}, level + 1))
601             {
602                 BufferDestroy(exp);
603                 return true;
604             }
605 
606             BufferDestroy(exp);
607         }
608 
609         break;
610 
611     case RVAL_TYPE_LIST:
612         for (const Rlist *rp = RvalRlistValue(rval); rp != NULL; rp = rp->next)
613         {
614             if (Epimenides(ctx, ns, scope, var, rp->val, level))
615             {
616                 return true;
617             }
618         }
619         break;
620 
621     case RVAL_TYPE_CONTAINER:
622     case RVAL_TYPE_FNCALL:
623     case RVAL_TYPE_NOPROMISEE:
624         return false;
625     }
626 
627     return false;
628 }
629 
630 /**
631  * @brief Collects variable constraints controlling how the promise should be converged
632  */
CollectConvergeVariableOptions(EvalContext * ctx,const Promise * pp)633 static ConvergeVariableOptions CollectConvergeVariableOptions(EvalContext *ctx, const Promise *pp)
634 {
635     ConvergeVariableOptions opts;
636     opts.drop_undefined = false;
637     opts.cp_save = NULL;                             /* main variable value */
638     /* By default allow variable redefinition, use "policy" constraint
639      * to override. */
640     opts.ok_redefine = true;
641     /* Main return value: becomes true at the end of the function. */
642     opts.should_converge = false;
643 
644     if (!IsDefinedClass(ctx, pp->classes))
645     {
646         return opts;
647     }
648 
649     int num_values = 0;
650     for (size_t i = 0; i < SeqLength(pp->conlist); i++)
651     {
652         Constraint *cp = SeqAt(pp->conlist, i);
653 
654         if (strcmp(cp->lval, "comment") == 0)
655         {
656             // Comments don't affect convergence
657             // Unclear why this is in the constraint list in the first place?
658             continue;
659         }
660         else if (cp->rval.item == NULL && cp->rval.type != RVAL_TYPE_LIST)
661         {
662             // No right value, considered empty
663             continue;
664         }
665         else if (strcmp(cp->lval, "ifvarclass") == 0 ||
666                  strcmp(cp->lval, "if")         == 0)
667         {
668             switch (cp->rval.type)
669             {
670             case RVAL_TYPE_SCALAR:
671                 if (!IsDefinedClass(ctx, cp->rval.item))
672                 {
673                     return opts;
674                 }
675 
676                 break;
677 
678             case RVAL_TYPE_FNCALL:
679             {
680                 bool excluded = false;
681 
682                 /* eval it: e.g. ifvarclass => not("a_class") */
683 
684                 Rval res = FnCallEvaluate(ctx, PromiseGetPolicy(pp), cp->rval.item, pp).rval;
685 
686                 /* Don't continue unless function was evaluated properly */
687                 if (res.type != RVAL_TYPE_SCALAR)
688                 {
689                     RvalDestroy(res);
690                     return opts;
691                 }
692 
693                 excluded = !IsDefinedClass(ctx, res.item);
694 
695                 RvalDestroy(res);
696 
697                 if (excluded)
698                 {
699                     return opts;
700                 }
701             }
702             break;
703 
704             default:
705                 Log(LOG_LEVEL_ERR, "Invalid if/ifvarclass type '%c': should be string or function", cp->rval.type);
706             }
707         }
708         else if (strcmp(cp->lval, "policy") == 0)
709         {
710             if (strcmp(cp->rval.item, "ifdefined") == 0)
711             {
712                 opts.drop_undefined = true;
713             }
714             else if (strcmp(cp->rval.item, "constant") == 0)
715             {
716                 opts.ok_redefine = false;
717             }
718         }
719         else if (DataTypeFromString(cp->lval) != CF_DATA_TYPE_NONE)
720         {
721             num_values++;
722             opts.cp_save = cp;
723         }
724     }
725 
726     if (opts.cp_save == NULL)
727     {
728         Log(LOG_LEVEL_WARNING, "Incomplete vars promise: %s",
729             pp->promiser);
730         PromiseRef(LOG_LEVEL_INFO, pp);
731         return opts;
732     }
733 
734     if (num_values > 2)
735     {
736         Log(LOG_LEVEL_ERR,
737             "Variable '%s' breaks its own promise with multiple (%d) values",
738             pp->promiser, num_values);
739         PromiseRef(LOG_LEVEL_ERR, pp);
740         return opts;
741     }
742 
743     /* All constraints look OK, and classes are defined. Move forward with
744      * this promise. */
745     opts.should_converge = true;
746 
747     return opts;
748 }
749