From: Arran Cudbard-Bell Date: Wed, 22 Apr 2026 18:32:48 +0000 (-0400) Subject: tmpl, value, xlat: carry null through arg lists instead of casting it X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5d93af264395eb11fe9e629eef3d3ee31aa5bbfe;p=thirdparty%2Ffreeradius-server.git tmpl, value, xlat: carry null through arg lists instead of casting it Reverses the cast coercion added in 3b5165084f. An explicit `null` should not silently become "" or zero-length octets - callers that wrote `null` meant "no value at all", which is a different shape from "the empty string". value.c: fr_value_box_cast_to_{string,octets} now return a clean fr_strerror() on FR_TYPE_NULL source instead of falling through to the catch-all fr_assert(0). xlat_tokenize.c: xlat_validate_function_arg skips the compile-time cast for FR_TYPE_NULL literals so a bareword `null` survives arg validation. xlat_eval.c: the runtime concat and per-box cast paths both pass an FR_TYPE_NULL source through to the xlat body unchanged, so implementations can check fr_type_is_null() on the incoming box and react accordingly. --- diff --git a/src/lib/unlang/xlat_eval.c b/src/lib/unlang/xlat_eval.c index d9e8fec9004..86ff38830b8 100644 --- a/src/lib/unlang/xlat_eval.c +++ b/src/lib/unlang/xlat_eval.c @@ -462,8 +462,12 @@ static xlat_action_t xlat_process_arg_list(TALLOC_CTX *ctx, fr_value_box_list_t /* * Concatenate child boxes, then cast to the desired type. + * Skip for an explicit `null` - concatenating would force + * a cast of FR_TYPE_NULL to the arg's declared type, which + * would either error or silently coerce to a zero-length + * value. Let the null box reach check_types intact. */ - if (concat) { + if (concat && !(fr_value_box_list_num_elements(list) == 1 && fr_type_is_null(vb->type))) { if (fr_value_box_list_concat_in_place(ctx, vb, list, type, FR_VALUE_BOX_LIST_FREE, true, SIZE_MAX) < 0) { RPEDEBUG("Function \"%s\" failed concatenating arguments to type %s", name, fr_type_to_str(type)); return XLAT_ACTION_FAIL; @@ -473,6 +477,8 @@ static xlat_action_t xlat_process_arg_list(TALLOC_CTX *ctx, fr_value_box_list_t goto check_types; } + if (concat) goto check_types; + /* * Only a single child box is valid here. Check there is * just one, cast to the correct type @@ -490,6 +496,16 @@ static xlat_action_t xlat_process_arg_list(TALLOC_CTX *ctx, fr_value_box_list_t check_types: if (!fr_type_is_leaf(arg->type)) goto check_non_leaf; + /* + * FR_TYPE_NULL is an explicit "no value" placeholder + * (the `null` keyword). Passing it through to the + * xlat body lets the implementation distinguish it + * from a zero-length value of the declared type; the + * author opted-in by writing `null` in the source. + * Casting it would paper over the distinction. + */ + if (fr_type_is_null(vb->type)) return XLAT_ACTION_DONE; + /* * Cast to the correct type if necessary. */ diff --git a/src/lib/unlang/xlat_tokenize.c b/src/lib/unlang/xlat_tokenize.c index 0bd3e04b91c..4ed9b39b333 100644 --- a/src/lib/unlang/xlat_tokenize.c +++ b/src/lib/unlang/xlat_tokenize.c @@ -340,6 +340,14 @@ static int xlat_validate_function_arg(xlat_arg_parser_t const *arg_p, xlat_exp_t return 0; } + /* + * An explicit `null` literal is preserved unchanged - the xlat + * body receives an FR_TYPE_NULL box in this arg slot and can + * decide what to do with it. Casting would collapse it into a + * zero-length value of the declared type and hide the intent. + */ + if (fr_type_is_null(node->data.type)) return 0; + /* * Cast (or parse) the input data to the expected argument data type. */ diff --git a/src/lib/util/value.c b/src/lib/util/value.c index 331c1caf2e1..234630126f3 100644 --- a/src/lib/util/value.c +++ b/src/lib/util/value.c @@ -2616,10 +2616,12 @@ static inline int fr_value_box_cast_to_strvalue(TALLOC_CTX *ctx, fr_value_box_t switch (src->type) { /* - * Explicit null casts to an empty string. + * An explicit `null` has no representation to cast from. + * Refuse rather than silently coerce to an empty string. */ case FR_TYPE_NULL: - return fr_value_box_bstrndup(ctx, dst, dst_enumv, "", 0, src->tainted); + fr_strerror_const("Cannot cast null to a string"); + return -1; /* * The presentation format of octets is hex @@ -2674,11 +2676,12 @@ static inline int fr_value_box_cast_to_octets(TALLOC_CTX *ctx, fr_value_box_t *d switch (src->type) { /* - * An explicit null (e.g. the `null` keyword in unlang) - * casts to a zero-length octets box. + * An explicit `null` has no representation to cast from. + * Refuse rather than silently coerce to zero-length octets. */ case FR_TYPE_NULL: - return fr_value_box_memdup(ctx, dst, dst_enumv, NULL, 0, src->tainted); + fr_strerror_const("Cannot cast null to octets"); + return -1; /* * (excluding terminating \0)