]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
tmpl, value: accept `null` as an explicit keyword
authorArran Cudbard-Bell <a.cudbardb@freeradius.org>
Wed, 22 Apr 2026 18:23:40 +0000 (14:23 -0400)
committerArran Cudbard-Bell <a.cudbardb@freeradius.org>
Wed, 22 Apr 2026 18:23:40 +0000 (14:23 -0400)
Adds tmpl_afrom_null_substr so the bareword `null` is recognised at
tmpl-tokenize time and builds a TMPL_TYPE_DATA wrapping an
FR_TYPE_NULL box.  Wired in before the numeric / address / bool /
attribute branches in tmpl_afrom_substr so a dictionary attribute
named "null" can't shadow it.

FR_TYPE_NULL previously doubled as the "uninitialised box" sentinel,
which is why TMPL_VERIFY panicked when it saw one inside a
TMPL_TYPE_DATA and why fr_value_box_cast_to_{string,octets} lacked
a source case for it.  With the null keyword those encounters are
now deliberate, so:

  - Drop the "FR_TYPE_NULL inside TMPL_TYPE_DATA is uninitialised"
    assertion in tmpl_tokenize.c's TMPL_VERIFY.
  - Cast FR_TYPE_NULL to an empty string / zero-length octets box.

The result is that positional xlat arguments can carry an explicit
"no value" placeholder without the framework dropping the slot or
the type system tripping over it.

src/lib/server/tmpl_tokenize.c
src/lib/util/value.c

index 5ccf6a018ac925214a5d62941119456b88039d83..53bc91801616e31271558f21867cf4f420a8e58a 100644 (file)
@@ -2616,6 +2616,41 @@ static fr_slen_t tmpl_afrom_value_substr(TALLOC_CTX *ctx, tmpl_t **out, fr_sbuff
        FR_SBUFF_SET_RETURN(in, &our_in);
 }
 
+/** Match the bareword `null` and return a TMPL_TYPE_DATA carrying an FR_TYPE_NULL box
+ *
+ * Used as an explicit "no value" placeholder by callers that want the
+ * argument slot to remain present (so positional xlat arguments line
+ * up) without carrying any bytes.  Downstream code distinguishes an
+ * intentional null from an uninitialised one by checking
+ * `fr_type_is_null(vb->type)` after the box has made it into an arg
+ * list - if it reaches the xlat body, the author put it there.
+ *
+ * @param[in] ctx     to allocate tmpl to.
+ * @param[out] out    where to write tmpl.
+ * @param[in] in      sbuff to parse.
+ * @param[in] p_rules formatting rules.
+ * @return
+ *     - 0 sbuff does not contain the `null` keyword.
+ *     - > 0 how many bytes were parsed.
+ */
+static fr_slen_t tmpl_afrom_null_substr(TALLOC_CTX *ctx, tmpl_t **out, fr_sbuff_t *in,
+                                       fr_sbuff_parse_rules_t const *p_rules)
+{
+       fr_sbuff_t      our_in = FR_SBUFF(in);
+       tmpl_t          *vpt;
+
+       if (!fr_sbuff_adv_past_strcase_literal(&our_in, "null")) return 0;
+       if (!tmpl_substr_terminal_check(&our_in, p_rules)) return 0;
+
+       MEM(vpt = tmpl_alloc(ctx, TMPL_TYPE_DATA, T_BARE_WORD,
+                            fr_sbuff_start(&our_in), fr_sbuff_used(&our_in)));
+       fr_value_box_init(&vpt->data.literal, FR_TYPE_NULL, NULL, false);
+
+       *out = vpt;
+
+       FR_SBUFF_SET_RETURN(in, &our_in);
+}
+
 /** Parse a truth value
  *
  * @param[in] ctx      to allocate tmpl to.
@@ -3380,6 +3415,16 @@ fr_slen_t tmpl_afrom_substr(TALLOC_CTX *ctx, tmpl_t **out,
                        fr_assert(!*out);
                }
 
+               /*
+                *      See if it's the `null` keyword.  Matched before the
+                *      numeric / address / enum branches so it isn't
+                *      shadowed by a dictionary attribute literally named
+                *      "null".
+                */
+               slen = tmpl_afrom_null_substr(ctx, out, &our_in, p_rules);
+               if (slen > 0) goto done_bareword;
+               fr_assert(!*out);
+
                /*
                 *      See if it's a boolean value
                 */
@@ -5354,11 +5399,12 @@ void tmpl_verify(char const *file, int line, tmpl_t const *vpt)
                                             file, line);
                }
 
-               if (fr_type_is_null(tmpl_value_type(vpt))) {
-                       fr_fatal_assert_fail("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA type was "
-                                            "FR_TYPE_NULL (uninitialised)", file, line);
-               }
-
+               /*
+                *      An FR_TYPE_NULL box inside a TMPL_TYPE_DATA used to
+                *      fire here as the "you forgot to init the box" signal,
+                *      but the `null` keyword (see tmpl_afrom_null_substr)
+                *      deliberately constructs one.  Accept it.
+                */
                if (tmpl_value_type(vpt) >= FR_TYPE_MAX) {
                        fr_fatal_assert_fail("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA type was "
                                             "%i (outside the range of fr_type_ts)", file, line, tmpl_value_type(vpt));
index 2d503144f2018df25ac6c4eccbc4e352f5d648d7..331c1caf2e11e15b753688e3281d27c7f05644cf 100644 (file)
@@ -2615,6 +2615,12 @@ static inline int fr_value_box_cast_to_strvalue(TALLOC_CTX *ctx, fr_value_box_t
        fr_value_box_init(dst, FR_TYPE_STRING, dst_enumv, false);
 
        switch (src->type) {
+       /*
+        *      Explicit null casts to an empty string.
+        */
+       case FR_TYPE_NULL:
+               return fr_value_box_bstrndup(ctx, dst, dst_enumv, "", 0, src->tainted);
+
        /*
         *      The presentation format of octets is hex
         *      What we actually want here is the raw string
@@ -2667,6 +2673,13 @@ static inline int fr_value_box_cast_to_octets(TALLOC_CTX *ctx, fr_value_box_t *d
        fr_value_box_safety_copy_changed(dst, src);
 
        switch (src->type) {
+       /*
+        *      An explicit null (e.g. the `null` keyword in unlang)
+        *      casts to a zero-length octets box.
+        */
+       case FR_TYPE_NULL:
+               return fr_value_box_memdup(ctx, dst, dst_enumv, NULL, 0, src->tainted);
+
        /*
         *      <string> (excluding terminating \0)
         */
@@ -2746,7 +2759,6 @@ static inline int fr_value_box_cast_to_octets(TALLOC_CTX *ctx, fr_value_box_t *d
        case FR_TYPE_VENDOR:
        case FR_TYPE_UNION:
        case FR_TYPE_INTERNAL:
-       case FR_TYPE_NULL:
        case FR_TYPE_ATTR:
        case FR_TYPE_COMBO_IP_ADDR: /* the types should have been realized to ipv4 / ipv6 */
        case FR_TYPE_COMBO_IP_PREFIX: