]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
update the tmpl_rules for argument parsing
authorAlan T. DeKok <aland@freeradius.org>
Sun, 9 Mar 2025 14:30:28 +0000 (10:30 -0400)
committerAlan T. DeKok <aland@freeradius.org>
Sun, 9 Mar 2025 18:15:53 +0000 (14:15 -0400)
so that we don't do casts, etc. of the function arguments

add more checks, and produce better errors

when checking function arguments, we no longer assume that all
of the arguments have been parsed as a string.  Instead, they
may be parsed as some other data type.  We can then cast the
input expression to the expected data type for the argument.

this change also changes the expected output, where functions like
%md5() now print their parsed arguments as octet strings.

src/bin/unit_test_attribute.c
src/lib/unlang/xlat_tokenize.c
src/tests/keywords/xlat-action-fail
src/tests/unit/condition/base.txt
src/tests/unit/condition/regex.txt
src/tests/unit/xlat/base.txt
src/tests/unit/xlat/cond_base.txt
src/tests/unit/xlat/expr.txt

index 8e7e3ee0a2faf082c4140f79d089abe91136b054..1a2093af298aa4e158abdd2a7e11ab9487c1e938 100644 (file)
@@ -3953,6 +3953,15 @@ int main(int argc, char *argv[])
        }
        xlat_func_args_set(xlat, xlat_test_args);
 
+       /*
+        *      And again WITHOUT arguments.
+        */
+       xlat = xlat_func_register(NULL, "test_no_args", xlat_test, FR_TYPE_NULL);
+       if (!xlat) {
+               ERROR("Failed registering xlat");
+               EXIT_WITH_FAILURE;
+       }
+
        /*
         *      Disable hostname lookups, so we don't produce spurious DNS
         *      queries, and there's no chance of spurious failures if
index 83d8893f99092b8a5018a3ca5b96db00900dd358..eb4d163338e83a76a0c10ac882e36f67f086cf73 100644 (file)
@@ -171,58 +171,95 @@ bool const xlat_func_chars[UINT8_MAX + 1] = {
 };
 
 
-static fr_slen_t xlat_validate_function_arg(xlat_arg_parser_t const *arg_p, xlat_exp_t *arg)
+static int xlat_validate_function_arg(xlat_arg_parser_t const *arg_p, xlat_exp_t *arg, int argc)
 {
-       ssize_t slen;
        xlat_exp_t *node;
-       fr_value_box_t box;
 
        /*
-        *      The caller doesn't care about the type, OR the type is string, which it already is.
+        *      The caller doesn't care about the type, we don't do any validation.
+        *
+        *      @todo - maybe check single / required?
         */
-       if ((arg_p->type == FR_TYPE_VOID) || (arg_p->type == FR_TYPE_STRING)) {
+       if (arg_p->type == FR_TYPE_VOID) {
                return 0;
        }
 
        node = xlat_exp_head(arg->group);
 
-       if (!node) return -1;
+       if (!node) {
+               if (!arg_p->required) return 0;
+
+               fr_strerror_const("Missing argument");
+               return -1;
+       }
 
        /*
         *      @todo - check arg_p->single, and complain.
         */
-       if (xlat_exp_next(arg->group, node)) return 0;
+       if (xlat_exp_next(arg->group, node)) {
+               return 0;
+       }
 
        /*
-        *      @todo - These checks are relatively basic.  We should do better checks, such as if the
-        *      expected type is not string/octets, and the passed arguments are multiple things, then
-        *      die?
-        *
-        *      And check also the 'concat' flag?
+        *      Hoist constant factors.
         */
-       if (node->type != XLAT_BOX) return 0;
+       if (node->type == XLAT_TMPL) {
+               /*
+                *      @todo - hoist the xlat, and then check the hoisted value again.
+                *      However, there seem to be few cases where this is used?
+                */
+               if (tmpl_is_xlat(node->vpt)) {
+                       return 0;
+
+                       /*
+                        *      Raw data can be hoisted to a value-box in this xlat node.
+                        */
+               } else if (tmpl_is_data(node->vpt)) {
+                       tmpl_t *vpt = node->vpt;
+
+                       fr_assert(tmpl_rules_cast(vpt) == FR_TYPE_NULL);
+
+                       fr_value_box_steal(node, &node->data, tmpl_value(vpt));
+
+                       talloc_free(vpt);
+                       xlat_exp_set_type(node, XLAT_BOX);
+
+               } else {
+                       fr_assert(!tmpl_is_data_unresolved(node->vpt));
+                       fr_assert(!tmpl_contains_regex(node->vpt));
+
+                       /*
+                        *      Can't cast the attribute / exec/ etc. to the expected data type of the
+                        *      argument, that has to happen at run-time.
+                        */
+                       return 0;
+               }
+       }
 
        /*
-        *      Boxes are always strings, because of xlat_tokenize_input()
+        *      @todo - These checks are relatively basic.  We should do better checks, such as if the
+        *      expected type is not string/octets, and the passed arguments are multiple things, then die?
+        *
+        *      If the node is pure, then we should arguably try to purify it now.
         */
-       fr_assert(node->data.type == FR_TYPE_STRING);
-
-       fr_value_box_init_null(&box);
+       if (node->type != XLAT_BOX) {
+               return 0;
+       }
 
        /*
-        *      The entire string must be parseable as the data type we expect.
+        *      If it's the correct data type, then we don't need to do anything.
         */
-       slen = fr_value_box_from_str(node, &box, arg_p->type, NULL, /* no enum */
-                                    node->data.vb_strvalue, node->data.vb_length,
-                                    NULL, /* no parse rules */
-                                    node->data.tainted);
-       if (slen <= 0) return slen;
+       if (arg_p->type == node->data.type) {
+               return 0;
+       }
 
        /*
-        *      Replace the string value with the parsed data type.
+        *      Cast (or parse) the input data to the expected argument data type.
         */
-       fr_value_box_clear(&node->data);
-       fr_value_box_copy(node, &node->data, &box);
+       if (fr_value_box_cast_in_place(node, &node->data, arg_p->type, NULL) < 0) {
+               fr_strerror_printf("Invalid argument %d - %s", argc, fr_strerror());
+               return -1;
+       }
 
        return 0;
 }
@@ -231,17 +268,15 @@ fr_slen_t xlat_validate_function_args(xlat_exp_t *node)
 {
        xlat_arg_parser_t const *arg_p;
        xlat_exp_t              *arg = xlat_exp_head(node->call.args);
-       int                     i = 0;
+       int                     i = 1;
 
        fr_assert(node->type == XLAT_FUNC);
 
        for (arg_p = node->call.func->args, i = 0; arg_p->type != FR_TYPE_NULL; arg_p++) {
-               fr_slen_t slen;
-
                if (!arg_p->required) break;
 
                if (!arg) {
-                       fr_strerror_printf("Missing required arg %u",
+                       fr_strerror_printf("Missing required argument %u",
                                           (unsigned int)(arg_p - node->call.func->args) + 1);
                        return -1;
                }
@@ -252,16 +287,17 @@ fr_slen_t xlat_validate_function_args(xlat_exp_t *node)
                 */
                fr_assert(arg->type == XLAT_GROUP);
 
-               slen = xlat_validate_function_arg(arg_p, arg);
-               if (slen < 0) {
-                       fr_strerror_printf("Failed parsing argument %d as type '%s'", i, fr_type_to_str(arg_p->type));
-                       return slen;
-               }
+               if (xlat_validate_function_arg(arg_p, arg, i) < 0) return -1;
 
                arg = xlat_exp_next(node->call.args, arg);
                i++;
        }
 
+       /*
+        *      @todo - check if there is a trailing argument.  But for functions which take no arguments, the
+        *      "arg" is an empty group.
+        */
+
        return 0;
 }
 
@@ -1342,6 +1378,8 @@ fr_slen_t xlat_tokenize_argv(TALLOC_CTX *ctx, xlat_exp_head_t **out, fr_sbuff_t
        fr_sbuff_parse_rules_t          tmp_p_rules;
        xlat_exp_head_t                 *head;
        xlat_arg_parser_t const         *arg = NULL, *arg_start;
+       tmpl_rules_t const              *our_t_rules = t_rules;
+       tmpl_rules_t                    tmp_t_rules;
 
        if (xlat && xlat->args) {
                arg_start = arg = xlat->args;   /* Track the arguments as we parse */
@@ -1371,6 +1409,28 @@ fr_slen_t xlat_tokenize_argv(TALLOC_CTX *ctx, xlat_exp_head_t **out, fr_sbuff_t
                fr_assert(p_rules->terminals);
 
                our_p_rules = p_rules;
+
+               /*
+                *      The arguments to a function are NOT the output data type of the function.
+                *
+                *      We do NOT check for quotation characters.  We DO update t_rules to strip any casts.  The
+                *      OUTPUT of the function is cast to the relevant data type, but each ARGUMENT is just an
+                *      expression with no given data type.  Parsing the expression is NOT done with the cast of
+                *      arg->type, as that means each individual piece of the expression is parsed as the type.  We
+                *      have to cast on the final _output_ of the expression, and we allow the _input_ pieces of the
+                *      expression to be just about anything.
+                */
+               if (!xlat_func_bare_words) {
+                       tmp_t_rules = *t_rules;
+                       our_t_rules = &tmp_t_rules;
+
+                       tmp_t_rules.enumv = NULL;
+                       tmp_t_rules.cast = FR_TYPE_NULL;
+                       tmp_t_rules.attr.namespace = NULL;
+                       tmp_t_rules.attr.request_def = NULL;
+                       tmp_t_rules.attr.list_def = request_attr_request;
+                       tmp_t_rules.attr.list_presence = TMPL_ATTR_LIST_ALLOW;
+               }
        }
 
        MEM(head = xlat_exp_head_alloc(ctx));
@@ -1395,13 +1455,19 @@ fr_slen_t xlat_tokenize_argv(TALLOC_CTX *ctx, xlat_exp_head_t **out, fr_sbuff_t
                 */
                if (!spaces) fr_sbuff_adv_past_whitespace(&our_in, SIZE_MAX, NULL);
 
-               fr_sbuff_out_by_longest_prefix(&slen, &quote, xlat_quote_table, &our_in, T_BARE_WORD);
-
                /*
                 *      Alloc a new node to hold the child nodes
                 *      that make up the argument.
                 */
                MEM(node = xlat_exp_alloc(head, XLAT_GROUP, NULL, 0));
+
+               if (!spaces && !xlat_func_bare_words) {
+                       quote = T_BARE_WORD;
+                       node->quote = quote;
+                       goto tokenize_expression;
+               }
+
+               fr_sbuff_out_by_longest_prefix(&slen, &quote, xlat_quote_table, &our_in, T_BARE_WORD);
                node->quote = quote;
 
                switch (quote) {
@@ -1433,14 +1499,17 @@ fr_slen_t xlat_tokenize_argv(TALLOC_CTX *ctx, xlat_exp_head_t **out, fr_sbuff_t
                                 */
                                slen = xlat_tokenize_input(node->group, &our_in, our_p_rules, t_rules, arg->safe_for);
 
-                       } else if (fr_sbuff_is_char(&our_in, ')')) {
-                               /*
-                                *      %foo()
-                                */
-                               slen = 0;
-
                        } else {
-                               slen = xlat_tokenize_expression(node, &node->group, &our_in, our_p_rules, t_rules);
+                       tokenize_expression:
+                               if (fr_sbuff_is_char(&our_in, ')')) {
+                                       /*
+                                        *      %foo()
+                                        */
+                                       slen = 0;
+
+                               } else {
+                                       slen = xlat_tokenize_expression(node, &node->group, &our_in, our_p_rules, our_t_rules);
+                               }
                        }
                        if (slen < 0) {
                        error:
@@ -1457,6 +1526,22 @@ fr_slen_t xlat_tokenize_argv(TALLOC_CTX *ctx, xlat_exp_head_t **out, fr_sbuff_t
                                fr_strerror_printf("Missing required arg %u", argc);
                                goto error;
                        }
+
+                       /*
+                        *      Validate the argument immediately on parsing it, and not later.
+                        */
+                       if (!spaces && slen) {
+                               if (arg->type == FR_TYPE_NULL) {
+                                       fr_strerror_printf("Too many arguments, expected %zu, got %d",
+                                                          (size_t) (arg - arg_start), argc);
+                                       goto error;
+                               }
+
+                               if (xlat_validate_function_arg(arg, node, argc) < 0) {
+                                       fr_sbuff_set(&our_in, &m);
+                                       goto error;
+                               }
+                       }
                        break;
 
                /*
index e098b2bcaf379ba6c9255e21670cf622ab1608fb..ec3fd305f75100c8692c929b2f39353bd5cdf40d 100644 (file)
@@ -1,7 +1,7 @@
 #
 #  A failed xlat is false.
 #
-if (!%debug(foobar)) {
+if (!%debug('foobar')) { # ERROR
        success
 
 } else {
index 62dd967c45be49aeb6f1348508d571113c2247e3..8d726fc2d62aab2408c00939009496634c2c1044 100644 (file)
@@ -241,13 +241,13 @@ condition (ipaddr)127.0.0.1 == "127.0.0.1"
 match true
 
 condition (ipaddr)127.0.0.1 == "%md4(' 127.0.0.1')"
-match (127.0.0.1 == "%md4(' 127.0.0.1')")
+match (127.0.0.1 == "%md4(0x203132372e302e302e31)")
 
 #
 #  Bare %{...} is allowed.
 #
 condition (ipaddr)127.0.0.1 == %md4('127.0.0.1')
-match (127.0.0.1 == %md4('127.0.0.1'))
+match (127.0.0.1 == %md4(0x3132372e302e302e31))
 
 condition (ipaddr)127.0.0.1 == %md4("SELECT user FROM table WHERE user='%{User-Name}'")
 match (127.0.0.1 == %md4("SELECT user FROM table WHERE user='%{User-Name}'"))
@@ -256,7 +256,7 @@ condition (ether) 00:11:22:33:44:55 == "00:11:22:33:44:55"
 match true
 
 condition (ether) 00:11:22:33:44:55 == "%md4('00:11:22:33:44:55')"
-match (00:11:22:33:44:55 == "%md4('00:11:22:33:44:55')")
+match (00:11:22:33:44:55 == "%md4(0x30303a31313a32323a33333a34343a3535)")
 
 condition (ether) 00:XX:22:33:44:55 == 00:11:22:33:44:55
 match ERROR offset 11: Unexpected text after attribute reference
@@ -310,10 +310,10 @@ condition ('foo' == 'foo')
 match true
 
 condition ("foo" == "%md4(' foo')")
-match ("foo" == "%md4(' foo')")
+match ("foo" == "%md4(0x20666f6f)")
 
 condition ("foo bar" == "%md4(' foo')")
-match ("foo bar" == "%md4(' foo')")
+match ("foo bar" == "%md4(0x20666f6f)")
 
 condition ("foo" == "bar")
 match false
@@ -330,7 +330,7 @@ condition (User-Name == "bob")
 match (User-Name == "bob")
 
 condition (User-Name == "%md4(' blah')")
-match (User-Name == "%md4(' blah')")
+match (User-Name == "%md4(0x20626c6168)")
 
 condition (ipaddr)127.0.0.1 == 2130706433
 match true
@@ -414,11 +414,7 @@ condition (string)"foo" == User-Name
 match ("foo" == User-Name)
 
 condition (integer)"%md4(' 1 + 1')" < NAS-Port
-match ((uint32)"%md4(' 1 + 1')" < NAS-Port)
-
-#
-#  The string gets parsed as an IP address.
-#
+match ((uint32)"%md4(0x2031202b2031)" < NAS-Port)
 
 condition Filter-Id == Framed-IP-Address
 match (Filter-Id == Framed-IP-Address)
index 64670fcbf6664f730638bdaa7979616e57cd12fa..2d0bed2cebd4860eaa1a416f9b4dc6692e56935c 100644 (file)
@@ -91,7 +91,7 @@ condition User-Name == /foo/
 match ERROR offset 14: Unexpected regular expression
 
 condition %md5("foo") =~ /foo/
-match ((string)%md5("foo") =~ /foo/)
+match ((string)%md5(0x666f6f) =~ /foo/)
 
 count
 match 50
index f7ccb44439a9c5450d6b53bf1de13d39b87b86ce..95f62d45747f84b003c09c6f8127298c62100744 100644 (file)
@@ -67,6 +67,9 @@ match ERROR offset 4: Missing closing brace
 xlat %{Tunnel-Password}
 match %{Tunnel-Password}
 
+xlat %test_no_args('foo')
+match ERROR offset 20: Too many arguments, expected 0, got 1
+
 xlat %test('bar')
 match %test('bar')
 
@@ -248,7 +251,7 @@ xlat %debug( 5)
 match %debug(5)
 
 xlat %debug("foo")
-match %debug("foo")
+match ERROR offset 8: Invalid argument 1 - Failed parsing string as type 'int8'
 
 #
 #  This is correct.
@@ -263,31 +266,36 @@ match %rpad(User-Name, 5, 'x')
 #  so the "offset" is wrong.  It also doesn't say *which* string is wrong.  We'll fix that later.
 #
 xlat %rpad(User-Name, 'foo', 'x')
-match ERROR offset 29: Failed parsing argument 1 as type 'uint64'
+match ERROR offset 17: Invalid argument 2 - Failed parsing string as type 'uint64'
 
 #
 #  Argument quoting
 #
 xlat %md5('"arg"')
-match %md5('"arg"')
+match %md5(0x2261726722)
 
+#
+#  Arguably this should be a parse error.  There's trailing text "}
+#  after the function call.
+#
+#  @todo - this is a side effect of command_xlat_normalize(), which passes in
+#  escape_rules_double, but does NOT look for leading or trailing ".  AND it
+#  doesn't pass in a terminal which says to stop at ".
+#
 xlat %md5('"arg')"}
-match %md5('"arg')\"}
+match %md5(0x22617267)\"}
 
 xlat %md5('"arg')
-match %md5('"arg')
+match %md5(0x22617267)
 
-#
-#  @todo - ??? not sure here
-#
 xlat %md5('"arg\""')
-match  %md5('"arg\\""')
+match %md5(0x226172675c2222)
 
 xlat %md5('arg')
-match %md5('arg')
+match %md5(0x617267)
 
 xlat %md5('arg"')
-match %md5('arg"')
+match %md5(0x61726722)
 
 count
-match 159
+match 161
index 137404631030f3697232c99808187c147a1e0e0d..b03eabbd7a619e49bad70ae599ea869720f4a998 100644 (file)
@@ -248,17 +248,17 @@ xlat_purify (ipaddr)127.0.0.1 == "127.0.0.1"
 match true
 
 # LHS is IPaddr, RHS is string (malformed IP address).
-# We can only fail this at run-time.
-xlat_purify (ipaddr)127.0.0.1 == "%md4(' 127.0.0.1')"
-match (127.0.0.1 == "%md4(' 127.0.0.1')")
-
+# We can only fail this at run-time, as the MD4 output
+# _might_ just accidentally be well-formed an IP address?
 #
-#  Bare %{...} is allowed.
+# i.e. the MD4 function doesn't define itself as returning
+# fixed length, just "octets".
 #
+xlat_purify (ipaddr)127.0.0.1 == "%md4(' 127.0.0.1')"
+match (127.0.0.1 == "%md4(0x203132372e302e302e31)")
 
-# Invalid cast from octets to ipaddr.
 xlat_purify (ipaddr)127.0.0.1 == %md4('127.0.0.1')
-match (127.0.0.1 == %md4('127.0.0.1'))
+match (127.0.0.1 == %md4(0x3132372e302e302e31))
 
 xlat_purify (ipaddr)127.0.0.1 == %md4("SELECT user FROM table WHERE user='%{User-Name}'")
 match (127.0.0.1 == %md4("SELECT user FROM table WHERE user='%{User-Name}'"))
@@ -266,9 +266,8 @@ match (127.0.0.1 == %md4("SELECT user FROM table WHERE user='%{User-Name}'"))
 xlat_purify (ether) 00:11:22:33:44:55 == "00:11:22:33:44:55"
 match true
 
-# Invalid cast from octets to ether.
 xlat_purify (ether)00:11:22:33:44:55 == "%md4('00:11:22:33:44:55')"
-match (00:11:22:33:44:55 == "%md4('00:11:22:33:44:55')")
+match (00:11:22:33:44:55 == "%md4(0x30303a31313a32323a33333a34343a3535)")
 
 xlat_purify (ether) 00:XX:22:33:44:55 == 00:11:22:33:44:55
 match ERROR offset 9: Unknown attributes not allowed here
index b8e08a8d59a316797c48b309c3146dedafecd1ff..ee0ec7268ddeb04c2dd6af72f744f0d432fdd5bf 100644 (file)
@@ -94,7 +94,7 @@ xlat_expr Filter-Id
 match Filter-Id
 
 xlat_expr %md5('foo') + "foo"
-match  (%md5('foo') + "foo")
+match (%md5(0x666f6f) + "foo")
 
 #  We can name the xlat's, tho we don't need to
 xlat_expr %op_add(4, 3) + 6