From 5f5c5c48b9edd3f2aa634d0d8e40ff77e116879e Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sun, 15 Sep 2024 13:49:32 +0900 Subject: [PATCH] udev-rules: support case insensitive match This introduces 'i' prefix for match string. When specified, string or pattern will match case-insensitively. Closes #34359. Co-authored-by: Ryan Wilson --- man/udev.xml | 6 + src/udev/fuzz-udev-rule-parse-value.c | 3 +- src/udev/test-udev-rules.c | 60 +++++---- src/udev/udev-rules.c | 170 ++++++++++++++++---------- src/udev/udev-rules.h | 2 +- test/test-udev.py | 11 ++ test/units/TEST-17-UDEV.11.sh | 4 + 7 files changed, 167 insertions(+), 89 deletions(-) diff --git a/man/udev.xml b/man/udev.xml index 6719c1b1c2e..e6c0e23ed47 100644 --- a/man/udev.xml +++ b/man/udev.xml @@ -141,6 +141,12 @@ For example, e"string\n" is parsed as 7 characters: 6 lowercase letters and a newline. This can be useful for writing special characters when a kernel driver requires them. + The string can be prefixed with a lowercase i (i"string") to mark that the string or pattern + will match case-insensitively. For example, i"foo" will match + foo, FOO, FoO and so on. The prefix can be + used only for match (==) or unmatch (!=) rules, e.g. + ATTR{foo}==i"abcd". + Please note that NUL is not allowed in either string variant. diff --git a/src/udev/fuzz-udev-rule-parse-value.c b/src/udev/fuzz-udev-rule-parse-value.c index 1817c15b3b4..57951bd4611 100644 --- a/src/udev/fuzz-udev-rule-parse-value.c +++ b/src/udev/fuzz-udev-rule-parse-value.c @@ -11,6 +11,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { int r; char *value = UINT_TO_PTR(0x12345678U); char *endpos = UINT_TO_PTR(0x87654321U); + bool is_case_sensitive; fuzz_setup_logging(); @@ -18,7 +19,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { memcpy(str, data, size); str[size] = '\0'; - r = udev_rule_parse_value(str, &value, &endpos); + r = udev_rule_parse_value(str, &value, &endpos, &is_case_sensitive); if (r < 0) { /* not modified on failure */ assert_se(value == UINT_TO_PTR(0x12345678U)); diff --git a/src/udev/test-udev-rules.c b/src/udev/test-udev-rules.c index b62b08b56ec..9e8ae87a8ca 100644 --- a/src/udev/test-udev-rules.c +++ b/src/udev/test-udev-rules.c @@ -4,15 +4,16 @@ #include "tests.h" #include "udev-rules.h" -static void test_udev_rule_parse_value_one(const char *in, const char *expected_value, int expected_retval) { +static void test_udev_rule_parse_value_one(const char *in, const char *expected_value, bool expected_case_insensitive, int expected_retval) { _cleanup_free_ char *str = NULL; char *value = UINT_TO_PTR(0x12345678U); char *endpos = UINT_TO_PTR(0x87654321U); + bool i; log_info("/* %s (%s, %s, %d) */", __func__, in, strnull(expected_value), expected_retval); assert_se(str = strdup(in)); - assert_se(udev_rule_parse_value(str, &value, &endpos) == expected_retval); + assert_se(udev_rule_parse_value(str, &value, &endpos, &i) == expected_retval); if (expected_retval < 0) { /* not modified on failure */ assert_se(value == UINT_TO_PTR(0x12345678U)); @@ -25,6 +26,7 @@ static void test_udev_rule_parse_value_one(const char *in, const char *expected_ * so it could be safely interpreted as nulstr. */ assert_se(value[strlen(value) + 1] == '\0'); + assert_se(i == expected_case_insensitive); } } @@ -33,45 +35,61 @@ TEST(udev_rule_parse_value) { * parsed: valid operand * use the following command to help generate textual C strings: * python3 -c 'import json; print(json.dumps(input()))' */ - test_udev_rule_parse_value_one("\"valid operand\"", "valid operand", 0); + test_udev_rule_parse_value_one("\"valid operand\"", "valid operand", /* case_insensitive = */ false, 0); /* input: "va'l\'id\"op\"erand" * parsed: va'l\'id"op"erand */ - test_udev_rule_parse_value_one("\"va'l\\'id\\\"op\\\"erand\"", "va'l\\'id\"op\"erand", 0); - test_udev_rule_parse_value_one("no quotes", NULL, -EINVAL); - test_udev_rule_parse_value_one("\"\\\\a\\b\\x\\y\"", "\\\\a\\b\\x\\y", 0); - test_udev_rule_parse_value_one("\"reject\0nul\"", NULL, -EINVAL); + test_udev_rule_parse_value_one("\"va'l\\'id\\\"op\\\"erand\"", "va'l\\'id\"op\"erand", /* case_insensitive = */ false, 0); + test_udev_rule_parse_value_one("no quotes", NULL, /* case_insensitive = */ false, -EINVAL); + test_udev_rule_parse_value_one("\"\\\\a\\b\\x\\y\"", "\\\\a\\b\\x\\y", /* case_insensitive = */ false, 0); + test_udev_rule_parse_value_one("\"reject\0nul\"", NULL, /* case_insensitive = */ false, -EINVAL); /* input: e"" */ - test_udev_rule_parse_value_one("e\"\"", "", 0); + test_udev_rule_parse_value_one("e\"\"", "", /* case_insensitive = */ false, 0); /* input: e"1234" */ - test_udev_rule_parse_value_one("e\"1234\"", "1234", 0); + test_udev_rule_parse_value_one("e\"1234\"", "1234", /* case_insensitive = */ false, 0); /* input: e"\"" */ - test_udev_rule_parse_value_one("e\"\\\"\"", "\"", 0); + test_udev_rule_parse_value_one("e\"\\\"\"", "\"", /* case_insensitive = */ false, 0); /* input: e"\ */ - test_udev_rule_parse_value_one("e\"\\", NULL, -EINVAL); + test_udev_rule_parse_value_one("e\"\\", NULL, /* case_insensitive = */ false, -EINVAL); /* input: e"\" */ - test_udev_rule_parse_value_one("e\"\\\"", NULL, -EINVAL); + test_udev_rule_parse_value_one("e\"\\\"", NULL, /* case_insensitive = */ false, -EINVAL); /* input: e"\\" */ - test_udev_rule_parse_value_one("e\"\\\\\"", "\\", 0); + test_udev_rule_parse_value_one("e\"\\\\\"", "\\", /* case_insensitive = */ false, 0); /* input: e"\\\" */ - test_udev_rule_parse_value_one("e\"\\\\\\\"", NULL, -EINVAL); + test_udev_rule_parse_value_one("e\"\\\\\\\"", NULL, /* case_insensitive = */ false, -EINVAL); /* input: e"\\\"" */ - test_udev_rule_parse_value_one("e\"\\\\\\\"\"", "\\\"", 0); + test_udev_rule_parse_value_one("e\"\\\\\\\"\"", "\\\"", /* case_insensitive = */ false, 0); /* input: e"\\\\" */ - test_udev_rule_parse_value_one("e\"\\\\\\\\\"", "\\\\", 0); + test_udev_rule_parse_value_one("e\"\\\\\\\\\"", "\\\\", /* case_insensitive = */ false, 0); /* input: e"operand with newline\n" */ - test_udev_rule_parse_value_one("e\"operand with newline\\n\"", "operand with newline\n", 0); + test_udev_rule_parse_value_one("e\"operand with newline\\n\"", "operand with newline\n", /* case_insensitive = */ false, 0); /* input: e"single\rcharacter\t\aescape\bsequence" */ test_udev_rule_parse_value_one( - "e\"single\\rcharacter\\t\\aescape\\bsequence\"", "single\rcharacter\t\aescape\bsequence", 0); + "e\"single\\rcharacter\\t\\aescape\\bsequence\"", "single\rcharacter\t\aescape\bsequence", /* case_insensitive = */ false, 0); /* input: e"reject\invalid escape sequence" */ - test_udev_rule_parse_value_one("e\"reject\\invalid escape sequence", NULL, -EINVAL); + test_udev_rule_parse_value_one("e\"reject\\invalid escape sequence", NULL, /* case_insensitive = */ false, -EINVAL); /* input: e"\ */ - test_udev_rule_parse_value_one("e\"\\", NULL, -EINVAL); + test_udev_rule_parse_value_one("e\"\\", NULL, /* case_insensitive = */ false, -EINVAL); /* input: "s\u1d1c\u1d04\u029c \u1d1c\u0274\u026a\u1d04\u1d0f\u1d05\u1d07 \U0001d568\U0001d560\U0001d568" */ test_udev_rule_parse_value_one( "e\"s\\u1d1c\\u1d04\\u029c \\u1d1c\\u0274\\u026a\\u1d04\\u1d0f\\u1d05\\u1d07 \\U0001d568\\U0001d560\\U0001d568\"", "s\xe1\xb4\x9c\xe1\xb4\x84\xca\x9c \xe1\xb4\x9c\xc9\xb4\xc9\xaa\xe1\xb4\x84\xe1\xb4\x8f\xe1\xb4\x85\xe1\xb4\x87 \xf0\x9d\x95\xa8\xf0\x9d\x95\xa0\xf0\x9d\x95\xa8", - 0); + /* case_insensitive = */ false, 0); + /* input: i"ABCD1234" */ + test_udev_rule_parse_value_one("i\"ABCD1234\"", "ABCD1234", /* case_insensitive = */ true, 0); + /* input: i"ABCD1234" */ + test_udev_rule_parse_value_one("e\"ABCD1234\"", "ABCD1234", /* case_insensitive = */ false, 0); + /* input: ei"\\"ABCD1234 */ + test_udev_rule_parse_value_one("ei\"\\\\ABCD1234\"", "\\ABCD1234", /* case_insensitive = */ true, 0); + /* input: ie"\\"ABCD1234 */ + test_udev_rule_parse_value_one("ie\"\\\\ABCD1234\"", "\\ABCD1234", /* case_insensitive = */ true, 0); + /* input: i */ + test_udev_rule_parse_value_one("i", NULL, /* case_insensitive = */ false, -EINVAL); + /* input: ee"" */ + test_udev_rule_parse_value_one("ee\"\"", NULL, /* case_insensitive = */ false, -EINVAL); + /* input: iei"" */ + test_udev_rule_parse_value_one("iei\"\"", NULL, /* case_insensitive = */ false, -EINVAL); + /* input: a"" */ + test_udev_rule_parse_value_one("a\"\"", NULL, /* case_insensitive = */ false, -EINVAL); } DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/udev/udev-rules.c b/src/udev/udev-rules.c index 26c5edc977b..85ae1c21632 100644 --- a/src/udev/udev-rules.c +++ b/src/udev/udev-rules.c @@ -66,6 +66,7 @@ typedef enum { _MATCH_TYPE_MASK = (1 << 5) - 1, MATCH_REMOVE_TRAILING_WHITESPACE = 1 << 5, /* Remove trailing whitespaces in attribute */ + MATCH_CASE_INSENSITIVE = 1 << 6, /* string or pattern is matched case-insensitively */ _MATCH_TYPE_INVALID = -EINVAL, } UdevRuleMatchType; @@ -299,6 +300,7 @@ struct UdevRules { #define log_line_invalid_op(line, key) _log_line_invalid_token(line, key, "operator") #define log_line_invalid_attr(line, key) _log_line_invalid_token(line, key, "attribute") +#define log_line_invalid_prefix(line, key) _log_line_invalid_token(line, key, "prefix 'i'") #define log_line_invalid_attr_format(line, key, attr, offset, hint) \ log_line_error_errno(line, SYNTHETIC_ERRNO(EINVAL), \ @@ -492,7 +494,7 @@ static bool type_has_nulstr_value(UdevRuleTokenType type) { return type < TK_M_TEST || type == TK_M_RESULT; } -static int rule_line_add_token(UdevRuleLine *rule_line, UdevRuleTokenType type, UdevRuleOperatorType op, char *value, void *data) { +static int rule_line_add_token(UdevRuleLine *rule_line, UdevRuleTokenType type, UdevRuleOperatorType op, char *value, void *data, bool is_case_insensitive) { _cleanup_(udev_rule_token_freep) UdevRuleToken *token = NULL; UdevRuleMatchType match_type = _MATCH_TYPE_INVALID; UdevRuleSubstituteType subst_type = _SUBST_TYPE_INVALID; @@ -567,6 +569,8 @@ static int rule_line_add_token(UdevRuleLine *rule_line, UdevRuleTokenType type, subst_type = rule_get_substitution_type(data); } + SET_FLAG(match_type, MATCH_CASE_INSENSITIVE, is_case_insensitive); + token = new(UdevRuleToken, 1); if (!token) return -ENOMEM; @@ -625,7 +629,7 @@ static int check_attr_format_and_warn(UdevRuleLine *line, const char *key, const return 0; } -static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, UdevRuleOperatorType op, char *value) { +static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, UdevRuleOperatorType op, char *value, bool is_case_insensitive) { ResolveNameTiming resolve_name_timing = LINE_GET_RULES(rule_line)->resolve_name_timing; bool is_match = IN_SET(op, OP_MATCH, OP_NOMATCH); int r; @@ -633,35 +637,39 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude assert(key); assert(value); + if (!is_match && is_case_insensitive) + return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), + "Invalid prefix 'i' for '%s'. The 'i' prefix can be specified only for '==' or '!=' operator.", key); + if (streq(key, "ACTION")) { if (attr) return log_line_invalid_attr(rule_line, key); if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_ACTION, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_ACTION, op, value, NULL, is_case_insensitive); } else if (streq(key, "DEVPATH")) { if (attr) return log_line_invalid_attr(rule_line, key); if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_DEVPATH, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_DEVPATH, op, value, NULL, is_case_insensitive); } else if (streq(key, "KERNEL")) { if (attr) return log_line_invalid_attr(rule_line, key); if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_KERNEL, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_KERNEL, op, value, NULL, is_case_insensitive); } else if (streq(key, "SYMLINK")) { if (attr) return log_line_invalid_attr(rule_line, key); if (!is_match) { check_value_format_and_warn(rule_line, key, value, false); - r = rule_line_add_token(rule_line, TK_A_DEVLINK, op, value, NULL); + r = rule_line_add_token(rule_line, TK_A_DEVLINK, op, value, NULL, /* is_case_insensitive = */ false); } else - r = rule_line_add_token(rule_line, TK_M_DEVLINK, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_DEVLINK, op, value, NULL, is_case_insensitive); } else if (streq(key, "NAME")) { if (attr) return log_line_invalid_attr(rule_line, key); @@ -681,9 +689,9 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude "Ignoring NAME=\"\", as udev will not delete any network interfaces."); check_value_format_and_warn(rule_line, key, value, false); - r = rule_line_add_token(rule_line, TK_A_NAME, op, value, NULL); + r = rule_line_add_token(rule_line, TK_A_NAME, op, value, NULL, /* is_case_insensitive = */ false); } else - r = rule_line_add_token(rule_line, TK_M_NAME, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_NAME, op, value, NULL, is_case_insensitive); } else if (streq(key, "ENV")) { if (isempty(attr)) return log_line_invalid_attr(rule_line, key); @@ -701,15 +709,15 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude check_value_format_and_warn(rule_line, key, value, false); - r = rule_line_add_token(rule_line, TK_A_ENV, op, value, attr); + r = rule_line_add_token(rule_line, TK_A_ENV, op, value, attr, /* is_case_insensitive = */ false); } else - r = rule_line_add_token(rule_line, TK_M_ENV, op, value, attr); + r = rule_line_add_token(rule_line, TK_M_ENV, op, value, attr, is_case_insensitive); } else if (streq(key, "CONST")) { if (isempty(attr) || !STR_IN_SET(attr, "arch", "virt")) return log_line_invalid_attr(rule_line, key); if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_CONST, op, value, attr); + r = rule_line_add_token(rule_line, TK_M_CONST, op, value, attr, is_case_insensitive); } else if (streq(key, "TAG")) { if (attr) return log_line_invalid_attr(rule_line, key); @@ -721,9 +729,9 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude if (!is_match) { check_value_format_and_warn(rule_line, key, value, true); - r = rule_line_add_token(rule_line, TK_A_TAG, op, value, NULL); + r = rule_line_add_token(rule_line, TK_A_TAG, op, value, NULL, /* is_case_insensitive = */ false); } else - r = rule_line_add_token(rule_line, TK_M_TAG, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_TAG, op, value, NULL, is_case_insensitive); } else if (streq(key, "SUBSYSTEM")) { if (attr) return log_line_invalid_attr(rule_line, key); @@ -733,14 +741,14 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude if (STR_IN_SET(value, "bus", "class")) log_line_warning(rule_line, "\"%s\" must be specified as \"subsystem\".", value); - r = rule_line_add_token(rule_line, TK_M_SUBSYSTEM, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_SUBSYSTEM, op, value, NULL, is_case_insensitive); } else if (streq(key, "DRIVER")) { if (attr) return log_line_invalid_attr(rule_line, key); if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_DRIVER, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_DRIVER, op, value, NULL, is_case_insensitive); } else if (streq(key, "ATTR")) { r = check_attr_format_and_warn(rule_line, key, attr); if (r < 0) @@ -754,9 +762,9 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude if (!is_match) { check_value_format_and_warn(rule_line, key, value, false); - r = rule_line_add_token(rule_line, TK_A_ATTR, op, value, attr); + r = rule_line_add_token(rule_line, TK_A_ATTR, op, value, attr, /* is_case_insensitive = */ false); } else - r = rule_line_add_token(rule_line, TK_M_ATTR, op, value, attr); + r = rule_line_add_token(rule_line, TK_M_ATTR, op, value, attr, is_case_insensitive); } else if (streq(key, "SYSCTL")) { r = check_attr_format_and_warn(rule_line, key, attr); if (r < 0) @@ -770,30 +778,30 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude if (!is_match) { check_value_format_and_warn(rule_line, key, value, false); - r = rule_line_add_token(rule_line, TK_A_SYSCTL, op, value, attr); + r = rule_line_add_token(rule_line, TK_A_SYSCTL, op, value, attr, /* is_case_insensitive = */ false); } else - r = rule_line_add_token(rule_line, TK_M_SYSCTL, op, value, attr); + r = rule_line_add_token(rule_line, TK_M_SYSCTL, op, value, attr, is_case_insensitive); } else if (streq(key, "KERNELS")) { if (attr) return log_line_invalid_attr(rule_line, key); if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_PARENTS_KERNEL, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_PARENTS_KERNEL, op, value, NULL, is_case_insensitive); } else if (streq(key, "SUBSYSTEMS")) { if (attr) return log_line_invalid_attr(rule_line, key); if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_PARENTS_SUBSYSTEM, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_PARENTS_SUBSYSTEM, op, value, NULL, is_case_insensitive); } else if (streq(key, "DRIVERS")) { if (attr) return log_line_invalid_attr(rule_line, key); if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_PARENTS_DRIVER, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_PARENTS_DRIVER, op, value, NULL, is_case_insensitive); } else if (streq(key, "ATTRS")) { r = check_attr_format_and_warn(rule_line, key, attr); if (r < 0) @@ -806,14 +814,14 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude if (strstr(attr, "../")) log_line_warning(rule_line, "Direct reference to parent sysfs directory, may break in future kernels."); - r = rule_line_add_token(rule_line, TK_M_PARENTS_ATTR, op, value, attr); + r = rule_line_add_token(rule_line, TK_M_PARENTS_ATTR, op, value, attr, is_case_insensitive); } else if (streq(key, "TAGS")) { if (attr) return log_line_invalid_attr(rule_line, key); if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_PARENTS_TAG, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_PARENTS_TAG, op, value, NULL, is_case_insensitive); } else if (streq(key, "TEST")) { mode_t mode = MODE_INVALID; @@ -825,8 +833,10 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude check_value_format_and_warn(rule_line, key, value, true); if (!is_match) return log_line_invalid_op(rule_line, key); + if (is_case_insensitive) + return log_line_invalid_prefix(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_TEST, op, value, MODE_TO_PTR(mode)); + r = rule_line_add_token(rule_line, TK_M_TEST, op, value, MODE_TO_PTR(mode), is_case_insensitive); } else if (streq(key, "PROGRAM")) { if (attr) return log_line_invalid_attr(rule_line, key); @@ -835,8 +845,10 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude return log_line_invalid_op(rule_line, key); if (!is_match) op = OP_MATCH; + if (is_case_insensitive) + return log_line_invalid_prefix(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_PROGRAM, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_PROGRAM, op, value, NULL, /* is_case_insensitive */ false); } else if (streq(key, "IMPORT")) { if (isempty(attr)) return log_line_invalid_attr(rule_line, key); @@ -845,18 +857,20 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude return log_line_invalid_op(rule_line, key); if (!is_match) op = OP_MATCH; + if (is_case_insensitive) + return log_line_invalid_prefix(rule_line, key); if (streq(attr, "file")) - r = rule_line_add_token(rule_line, TK_M_IMPORT_FILE, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_IMPORT_FILE, op, value, NULL, /* is_case_insensitive = */ false); else if (streq(attr, "program")) { UdevBuiltinCommand cmd; cmd = udev_builtin_lookup(value); if (cmd >= 0) { log_line_debug(rule_line, "Found builtin command '%s' for %s, replacing attribute.", value, key); - r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd)); + r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd), /* is_case_insensitive = */ false); } else - r = rule_line_add_token(rule_line, TK_M_IMPORT_PROGRAM, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_IMPORT_PROGRAM, op, value, NULL, /* is_case_insensitive = */ false); } else if (streq(attr, "builtin")) { UdevBuiltinCommand cmd; @@ -864,13 +878,13 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude if (cmd < 0) return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), "Unknown builtin command: %s", value); - r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd)); + r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd), /* is_case_insensitive = */ false); } else if (streq(attr, "db")) - r = rule_line_add_token(rule_line, TK_M_IMPORT_DB, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_IMPORT_DB, op, value, NULL, /* is_case_insensitive = */ false); else if (streq(attr, "cmdline")) - r = rule_line_add_token(rule_line, TK_M_IMPORT_CMDLINE, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_IMPORT_CMDLINE, op, value, NULL, /* is_case_insensitive = */ false); else if (streq(attr, "parent")) - r = rule_line_add_token(rule_line, TK_M_IMPORT_PARENT, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_IMPORT_PARENT, op, value, NULL, /* is_case_insensitive = */ false); else return log_line_invalid_attr(rule_line, key); } else if (streq(key, "RESULT")) { @@ -879,7 +893,7 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude if (!is_match) return log_line_invalid_op(rule_line, key); - r = rule_line_add_token(rule_line, TK_M_RESULT, op, value, NULL); + r = rule_line_add_token(rule_line, TK_M_RESULT, op, value, NULL, is_case_insensitive); } else if (streq(key, "OPTIONS")) { char *tmp; @@ -891,24 +905,24 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude op = OP_ASSIGN; if (streq(value, "string_escape=none")) - r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_NONE, op, NULL, NULL); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_NONE, op, NULL, NULL, /* is_case_insensitive = */ false); else if (streq(value, "string_escape=replace")) - r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_REPLACE, op, NULL, NULL); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_REPLACE, op, NULL, NULL, /* is_case_insensitive = */ false); else if (streq(value, "db_persist")) - r = rule_line_add_token(rule_line, TK_A_OPTIONS_DB_PERSIST, op, NULL, NULL); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_DB_PERSIST, op, NULL, NULL, /* is_case_insensitive = */ false); else if (streq(value, "watch")) - r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(1)); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(1), /* is_case_insensitive = */ false); else if (streq(value, "nowatch")) - r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(0)); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(0), /* is_case_insensitive = */ false); else if ((tmp = startswith(value, "static_node="))) - r = rule_line_add_token(rule_line, TK_A_OPTIONS_STATIC_NODE, op, tmp, NULL); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_STATIC_NODE, op, tmp, NULL, /* is_case_insensitive = */ false); else if ((tmp = startswith(value, "link_priority="))) { int prio; r = safe_atoi(tmp, &prio); if (r < 0) return log_line_error_errno(rule_line, r, "Failed to parse link priority '%s': %m", tmp); - r = rule_line_add_token(rule_line, TK_A_OPTIONS_DEVLINK_PRIORITY, op, NULL, INT_TO_PTR(prio)); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_DEVLINK_PRIORITY, op, NULL, INT_TO_PTR(prio), /* is_case_insensitive = */ false); } else if ((tmp = startswith(value, "log_level="))) { int level; @@ -919,7 +933,7 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude if (level < 0) return log_line_error_errno(rule_line, level, "Failed to parse log level '%s': %m", tmp); } - r = rule_line_add_token(rule_line, TK_A_OPTIONS_LOG_LEVEL, op, NULL, INT_TO_PTR(level)); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_LOG_LEVEL, op, NULL, INT_TO_PTR(level), /* is_case_insensitive = */ false); } else { log_line_warning(rule_line, "Invalid value for OPTIONS key, ignoring: '%s'", value); return 0; @@ -937,17 +951,17 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude } if (parse_uid(value, &uid) >= 0) - r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid)); + r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid), /* is_case_insensitive = */ false); else if (resolve_name_timing == RESOLVE_NAME_EARLY && rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) { r = rule_resolve_user(rule_line, value, &uid); if (r < 0) return log_line_error_errno(rule_line, r, "Failed to resolve user name '%s': %m", value); - r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid)); + r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid), /* is_case_insensitive = */ false); } else if (resolve_name_timing != RESOLVE_NAME_NEVER) { check_value_format_and_warn(rule_line, key, value, true); - r = rule_line_add_token(rule_line, TK_A_OWNER, op, value, NULL); + r = rule_line_add_token(rule_line, TK_A_OWNER, op, value, NULL, /* is_case_insensitive = */ false); } else { log_line_debug(rule_line, "User name resolution is disabled, ignoring %s=\"%s\".", key, value); return 0; @@ -965,17 +979,17 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude } if (parse_gid(value, &gid) >= 0) - r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid)); + r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid), /* is_case_insensitive = */ false); else if (resolve_name_timing == RESOLVE_NAME_EARLY && rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) { r = rule_resolve_group(rule_line, value, &gid); if (r < 0) return log_line_error_errno(rule_line, r, "Failed to resolve group name '%s': %m", value); - r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid)); + r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid), /* is_case_insensitive = */ false); } else if (resolve_name_timing != RESOLVE_NAME_NEVER) { check_value_format_and_warn(rule_line, key, value, true); - r = rule_line_add_token(rule_line, TK_A_GROUP, op, value, NULL); + r = rule_line_add_token(rule_line, TK_A_GROUP, op, value, NULL, /* is_case_insensitive = */ false); } else { log_line_debug(rule_line, "Resolving group name is disabled, ignoring GROUP=\"%s\".", value); return 0; @@ -993,10 +1007,10 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude } if (parse_mode(value, &mode) >= 0) - r = rule_line_add_token(rule_line, TK_A_MODE_ID, op, NULL, MODE_TO_PTR(mode)); + r = rule_line_add_token(rule_line, TK_A_MODE_ID, op, NULL, MODE_TO_PTR(mode), /* is_case_insensitive = */ false); else { check_value_format_and_warn(rule_line, key, value, true); - r = rule_line_add_token(rule_line, TK_A_MODE, op, value, NULL); + r = rule_line_add_token(rule_line, TK_A_MODE, op, value, NULL, /* is_case_insensitive = */ false); } } else if (streq(key, "SECLABEL")) { if (isempty(attr)) @@ -1009,13 +1023,13 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude op = OP_ASSIGN; } - r = rule_line_add_token(rule_line, TK_A_SECLABEL, op, value, attr); + r = rule_line_add_token(rule_line, TK_A_SECLABEL, op, value, attr, /* is_case_insensitive = */ false); } else if (streq(key, "RUN")) { if (is_match || op == OP_REMOVE) return log_line_invalid_op(rule_line, key); check_value_format_and_warn(rule_line, key, value, true); if (!attr || streq(attr, "program")) - r = rule_line_add_token(rule_line, TK_A_RUN_PROGRAM, op, value, NULL); + r = rule_line_add_token(rule_line, TK_A_RUN_PROGRAM, op, value, NULL, /* is_case_insensitive = */ false); else if (streq(attr, "builtin")) { UdevBuiltinCommand cmd; @@ -1023,7 +1037,7 @@ static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, Ude if (cmd < 0) return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), "Unknown builtin command '%s', ignoring.", value); - r = rule_line_add_token(rule_line, TK_A_RUN_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd)); + r = rule_line_add_token(rule_line, TK_A_RUN_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd), /* is_case_insensitive = */ false); } else return log_line_invalid_attr(rule_line, key); } else if (streq(key, "GOTO")) { @@ -1123,13 +1137,30 @@ static void check_token_delimiters(UdevRuleLine *rule_line, const char *line) { } } -int udev_rule_parse_value(char *str, char **ret_value, char **ret_endpos) { +int udev_rule_parse_value(char *str, char **ret_value, char **ret_endpos, bool *ret_is_case_insensitive) { char *i, *j; - bool is_escaped; + bool is_escaped = false, is_case_insensitive = false; + + assert(str); + assert(ret_value); + assert(ret_endpos); + assert(ret_is_case_insensitive); + + /* check if string is prefixed with: + * - "e" for escaped + * - "i" for case insensitive match + * + * Note both e and i can be set but do not allow duplicates ("eei", "eii"). */ + for (const char *k = str; *k != '"' && k < str + 2; k++) + if (*k == 'e' && !is_escaped) + is_escaped = true; + else if (*k == 'i' && !is_case_insensitive) + is_case_insensitive = true; + else + return -EINVAL; /* value must be double quotated */ - is_escaped = str[0] == 'e'; - str += is_escaped; + str += is_escaped + is_case_insensitive; if (str[0] != '"') return -EINVAL; @@ -1176,10 +1207,11 @@ int udev_rule_parse_value(char *str, char **ret_value, char **ret_endpos) { *ret_value = str; *ret_endpos = i + 1; + *ret_is_case_insensitive = is_case_insensitive; return 0; } -static int parse_line(char **line, char **ret_key, char **ret_attr, UdevRuleOperatorType *ret_op, char **ret_value) { +static int parse_line(char **line, char **ret_key, char **ret_attr, UdevRuleOperatorType *ret_op, char **ret_value, bool *ret_is_case_insensitive) { char *key_begin, *key_end, *attr, *tmp; UdevRuleOperatorType op; int r; @@ -1189,6 +1221,7 @@ static int parse_line(char **line, char **ret_key, char **ret_attr, UdevRuleOper assert(ret_key); assert(ret_op); assert(ret_value); + assert(ret_is_case_insensitive); key_begin = skip_leading_chars(*line, WHITESPACE ","); @@ -1223,7 +1256,7 @@ static int parse_line(char **line, char **ret_key, char **ret_attr, UdevRuleOper tmp += op == OP_ASSIGN ? 1 : 2; tmp = skip_leading_chars(tmp, NULL); - r = udev_rule_parse_value(tmp, ret_value, line); + r = udev_rule_parse_value(tmp, ret_value, line, ret_is_case_insensitive); if (r < 0) return r; @@ -1295,17 +1328,18 @@ static int rule_add_line(UdevRuleFile *rule_file, const char *line_str, unsigned for (p = rule_line->line; !isempty(p); ) { char *key, *attr, *value; UdevRuleOperatorType op; + bool is_case_insensitive; if (extra_checks) check_token_delimiters(rule_line, p); - r = parse_line(&p, &key, &attr, &op, &value); + r = parse_line(&p, &key, &attr, &op, &value, &is_case_insensitive); if (r < 0) return log_line_error_errno(rule_line, r, "Invalid key/value pair, ignoring."); if (r == 0) break; - r = parse_token(rule_line, key, attr, op, value); + r = parse_token(rule_line, key, attr, op, value, is_case_insensitive); if (r < 0) return r; } @@ -1698,7 +1732,7 @@ bool udev_rules_should_reload(UdevRules *rules) { static bool token_match_string(UdevRuleToken *token, const char *str) { const char *value; - bool match = false; + bool match = false, case_insensitive; assert(token); assert(token->value); @@ -1706,13 +1740,17 @@ static bool token_match_string(UdevRuleToken *token, const char *str) { str = strempty(str); value = token->value; + case_insensitive = FLAGS_SET(token->match_type, MATCH_CASE_INSENSITIVE); switch (token->match_type & _MATCH_TYPE_MASK) { case MATCH_TYPE_EMPTY: match = isempty(str); break; case MATCH_TYPE_SUBSYSTEM: - match = STR_IN_SET(str, "subsystem", "class", "bus"); + if (case_insensitive) + match = STRCASE_IN_SET(str, "subsystem", "class", "bus"); + else + match = STR_IN_SET(str, "subsystem", "class", "bus"); break; case MATCH_TYPE_PLAIN_WITH_EMPTY: if (isempty(str)) { @@ -1722,7 +1760,7 @@ static bool token_match_string(UdevRuleToken *token, const char *str) { _fallthrough_; case MATCH_TYPE_PLAIN: NULSTR_FOREACH(i, value) - if (streq(i, str)) { + if (case_insensitive ? strcaseeq(i, str) : streq(i, str)) { match = true; break; } @@ -1735,7 +1773,7 @@ static bool token_match_string(UdevRuleToken *token, const char *str) { _fallthrough_; case MATCH_TYPE_GLOB: NULSTR_FOREACH(i, value) - if ((fnmatch(i, str, 0) == 0)) { + if ((fnmatch(i, str, case_insensitive ? FNM_CASEFOLD : 0) == 0)) { match = true; break; } diff --git a/src/udev/udev-rules.h b/src/udev/udev-rules.h index ceb454da8d6..61d517d9da5 100644 --- a/src/udev/udev-rules.h +++ b/src/udev/udev-rules.h @@ -29,7 +29,7 @@ typedef enum ResolveNameTiming { _RESOLVE_NAME_TIMING_INVALID = -EINVAL, } ResolveNameTiming; -int udev_rule_parse_value(char *str, char **ret_value, char **ret_endpos); +int udev_rule_parse_value(char *str, char **ret_value, char **ret_endpos, bool *ret_is_case_insensitive); int udev_rules_parse_file(UdevRules *rules, const char *filename, bool extra_checks, UdevRuleFile **ret); unsigned udev_rule_file_get_issues(UdevRuleFile *rule_file); UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing); diff --git a/test/test-udev.py b/test/test-udev.py index d9d840eb8ce..68c48fd7907 100755 --- a/test/test-udev.py +++ b/test/test-udev.py @@ -2313,6 +2313,17 @@ SUBSYSTEMS=="scsi", PROGRAM=="/bin/sh -c \"printf %%s 'foo1 foo2' | grep 'foo1 f SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNEL=="sd*", SYMLINK+="blockdev" KERNEL=="sda6", OPTIONS+="link_priority=10" """), + + Rules.new( + "case insensitive match", + Device( + "/devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda/sda1", + exp_links = ["ok"], + ), + + rules = r""" + KERNEL==i"SDA1", SUBSYSTEMS==i"SCSI", ATTRS{vendor}==i"a?a", SYMLINK+="ok" + """), ] def fork_and_run_udev(action: str, rules: Rules) -> None: diff --git a/test/units/TEST-17-UDEV.11.sh b/test/units/TEST-17-UDEV.11.sh index 42b925f60b3..8413d3c1898 100755 --- a/test/units/TEST-17-UDEV.11.sh +++ b/test/units/TEST-17-UDEV.11.sh @@ -237,6 +237,8 @@ test_syntax_error 'ENV=="b"' 'Invalid attribute for ENV.' test_syntax_error 'ENV{a}-="b"' 'Invalid operator for ENV.' test_syntax_error 'ENV{a}:="b"' "ENV key takes '==', '!=', '=', or '+=' operator, assuming '='." test_syntax_error 'ENV{ACTION}="b"' "Invalid ENV attribute. 'ACTION' cannot be set." +test_syntax_error 'ENV{a}=i"b"' "Invalid prefix 'i' for 'ENV'. The 'i' prefix can be specified only for '==' or '!=' operator." +test_syntax_error 'ENV{a}+=i"b"' "Invalid prefix 'i' for 'ENV'. The 'i' prefix can be specified only for '==' or '!=' operator." test_syntax_error 'CONST=="b"' 'Invalid attribute for CONST.' test_syntax_error 'CONST{a}=="b"' 'Invalid attribute for CONST.' test_syntax_error 'CONST{arch}="b"' 'Invalid operator for CONST.' @@ -275,10 +277,12 @@ test_syntax_error 'TEST{0644}="b"' 'Invalid operator for TEST.' test_syntax_error 'PROGRAM{a}=="b"' 'Invalid attribute for PROGRAM.' test_syntax_error 'PROGRAM-="b"' 'Invalid operator for PROGRAM.' test_syntax_error 'PROGRAM=="%", NAME="b"' 'Invalid value "%" for PROGRAM (char 1: invalid substitution type), ignoring.' +test_syntax_error 'PROGRAM==i"b"' "Invalid prefix 'i' for PROGRAM." test_syntax_error 'IMPORT="b"' 'Invalid attribute for IMPORT.' test_syntax_error 'IMPORT{a}="b"' 'Invalid attribute for IMPORT.' test_syntax_error 'IMPORT{a}-="b"' 'Invalid operator for IMPORT.' test_syntax_error 'IMPORT{file}=="%", NAME="b"' 'Invalid value "%" for IMPORT (char 1: invalid substitution type), ignoring.' +test_syntax_error 'IMPORT{file}==i"a", NAME="b"' "Invalid prefix 'i' for IMPORT." test_syntax_error 'IMPORT{builtin}!="foo"' 'Unknown builtin command: foo' test_syntax_error 'RESULT{a}=="b"' 'Invalid attribute for RESULT.' test_syntax_error 'RESULT:="b"' 'Invalid operator for RESULT.'