From 3b5165084f1ac1e9c81d1bdbfa5a8fc32ca490fb Mon Sep 17 00:00:00 2001 From: Arran Cudbard-Bell Date: Wed, 22 Apr 2026 14:23:40 -0400 Subject: [PATCH] tmpl, value: accept `null` as an explicit keyword 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 | 56 +++++++++++++++++++++++++++++++--- src/lib/util/value.c | 14 ++++++++- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/lib/server/tmpl_tokenize.c b/src/lib/server/tmpl_tokenize.c index 5ccf6a018ac..53bc9180161 100644 --- a/src/lib/server/tmpl_tokenize.c +++ b/src/lib/server/tmpl_tokenize.c @@ -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)); diff --git a/src/lib/util/value.c b/src/lib/util/value.c index 2d503144f20..331c1caf2e1 100644 --- a/src/lib/util/value.c +++ b/src/lib/util/value.c @@ -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); + /* * (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: -- 2.47.3