]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
udevadm: introduce new 'verify' command
authorDmitry V. Levin <ldv@strace.io>
Thu, 2 Mar 2023 08:00:00 +0000 (08:00 +0000)
committerDmitry V. Levin <ldv@strace.io>
Wed, 8 Mar 2023 18:55:40 +0000 (18:55 +0000)
We seem to have no tool to verify udev rule files.  There is a simple
udev rules syntax checker in the tree, test/rule-syntax-check.py, but
it is too simple to detect less trivial issues not detected by udev,
e.g. redundant comparisons (#26593) or labels without references.

Such a tool would be beneficial not only for maintaining udev rules
distributed along with udev, but also and even more so for maintaining
third party udev rules that are more likely to have issues with syntax
and semantic correctness.

Implement a udev rules syntax and semantics checker in the form of
'udevadm verify [OPTIONS] FILE...' command that is based on
udev_rules_parse_file() interface and would apply further checks
on top of it in subsequent commits.

Resolves: #26606

man/udevadm.xml
shell-completion/bash/udevadm
shell-completion/zsh/_udevadm
src/udev/meson.build
src/udev/udev-rules.c
src/udev/udev-rules.h
src/udev/udevadm-verify.c [new file with mode: 0644]
src/udev/udevadm.c
src/udev/udevadm.h

index 33af1550213cc6312f96c01320a2fd136eee2d6a..95b8da2b0a48c3f97384a64e0f61ad080c429c7d 100644 (file)
     <cmdsynopsis>
       <command>udevadm test-builtin <optional>options</optional> <replaceable>command</replaceable> <replaceable>devpath</replaceable></command>
     </cmdsynopsis>
+    <cmdsynopsis>
+      <command>udevadm verify</command>
+      <arg choice="opt" rep="repeat">options</arg>
+      <arg choice="plain" rep="repeat"><replaceable>file</replaceable></arg>
+    </cmdsynopsis>
     <cmdsynopsis>
       <command>udevadm wait <optional>options</optional> <replaceable>device|syspath</replaceable></command>
     </cmdsynopsis>
       </variablelist>
     </refsect2>
 
+    <refsect2>
+      <title>udevadm verify
+      <arg choice="opt"><replaceable>options</replaceable></arg>
+      <arg choice="plain" rep="repeat"><replaceable>file</replaceable></arg>
+      …
+      </title>
+
+      <para>Verify syntactic and semantic correctness of udev rules files.</para>
+
+      <para>Positional arguments should be used to specify one or more files to check.</para>
+
+      <para>The exit status is <constant>0</constant> if all specified udev rules files
+      are syntactically and semantically correct, and a non-zero error code otherwise.</para>
+
+      <variablelist>
+        <varlistentry>
+          <term><option>-N</option></term>
+          <term><option>--resolve-names=<constant>early</constant>|<constant>never</constant></option></term>
+          <listitem>
+            <para>Specify when udevadm should resolve names of users
+            and groups.  When set to <constant>early</constant> (the
+            default), names will be resolved when the rules are
+            parsed. When set to <constant>never</constant>, names will
+            never be resolved.</para>
+          </listitem>
+        </varlistentry>
+
+        <xi:include href="standard-options.xml" xpointer="help" />
+      </variablelist>
+    </refsect2>
+
     <refsect2>
       <title>udevadm wait
       <arg choice="opt"><replaceable>options</replaceable></arg>
index 99d4aa55c98130d38ea89d141cd039a7e8fdac13..b6e14e1d36e777705215a551142933c6a53e07ef 100644 (file)
@@ -64,11 +64,12 @@ _udevadm() {
         [MONITOR_ARG]='-s --subsystem-match -t --tag-match'
         [TEST]='-a --action -N --resolve-names'
         [TEST_BUILTIN]='-a --action'
+        [VERIFY]='-N --resolve-names'
         [WAIT]='-t --timeout --initialized=no --removed --settle'
         [LOCK]='-t --timeout -d --device -b --backing -p --print'
     )
 
-    local verbs=(info trigger settle control monitor test-builtin test wait lock)
+    local verbs=(info trigger settle control monitor test-builtin test verify wait lock)
     local builtins=(blkid btrfs hwdb input_id keyboard kmod net_id net_setup_link path_id usb_id uaccess)
 
     for ((i=0; i < COMP_CWORD; i++)); do
@@ -247,6 +248,27 @@ _udevadm() {
             fi
             ;;
 
+        'verify')
+            if __contains_word "$prev" ${OPTS[VERIFY]}; then
+                case $prev in
+                    -N|--resolve-names)
+                        comps='early never'
+                        ;;
+                    *)
+                        comps=''
+                        ;;
+                esac
+                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+                return 0
+            fi
+
+            if [[ $cur = -* ]]; then
+                comps="${OPTS[COMMON]} ${OPTS[VERIFY]}"
+            else
+                comps=$( compgen -A file -- "$cur" )
+            fi
+            ;;
+
         'wait')
             if __contains_word "$prev" ${OPTS[WAIT]}; then
                 case $prev in
index f7c3384eae2ba3608dbae2d0ab166be03431eb51..074d367a9de18115dff6e2565c1b72cce5a69064 100644 (file)
@@ -104,6 +104,14 @@ _udevadm_test-builtin(){
     fi
 }
 
+(( $+functions[_udevadm_verify] )) ||
+_udevadm_verify(){
+    _arguments \
+        {-N+,--resolve-names=}'[When to resolve names.]:resolve:(early never)' \
+        {-h,--help}'[Print help text.]' \
+        '*::files:_files'
+}
+
 (( $+functions[_udevadm_wait] )) ||
 _udevadm_wait(){
     _arguments \
@@ -154,6 +162,7 @@ _udevadm_commands(){
         'monitor:listen to kernel and udev events'
         'test:test an event run'
         'test-builtin:test a built-in command'
+        'verify:verify udev rules files'
         'wait:wait for devices or device symlinks being created'
         'lock:lock a block device and run a comand'
     )
index 3f4ab00ac9506df3cb84d9e45576318a367f7e9e..1cac581e7fe8bceb50377b0694ccb339367dfb08 100644 (file)
@@ -12,6 +12,7 @@ udevadm_sources = files(
         'udevadm-test-builtin.c',
         'udevadm-trigger.c',
         'udevadm-util.c',
+        'udevadm-verify.c',
         'udevadm-wait.c',
         'udevd.c',
 )
index 840448d65c3942a107e570a14618ca1587567b62..c2e98e9a9e82efc293b758e12b1e0354ba1b077b 100644 (file)
@@ -1321,6 +1321,16 @@ int udev_rules_parse_file(UdevRules *rules, const char *filename) {
         return 0;
 }
 
+unsigned udev_check_current_rule_file(UdevRules *rules) {
+        assert(rules);
+
+        UdevRuleFile *rule_file = rules->current_file;
+        if (!rule_file)
+                return 0;
+
+        return rule_file->issues;
+}
+
 UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing) {
         assert(resolve_name_timing >= 0 && resolve_name_timing < _RESOLVE_NAME_TIMING_MAX);
 
index 860fe7c8e4e6cc01029f7de82b063b7e8a38d535..44f30e1c137e849857334e531ca4e105a772a55d 100644 (file)
@@ -18,6 +18,7 @@ typedef enum {
 } UdevRuleEscapeType;
 
 int udev_rules_parse_file(UdevRules *rules, const char *filename);
+unsigned udev_check_current_rule_file(UdevRules *rules);
 UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing);
 int udev_rules_load(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing);
 UdevRules *udev_rules_free(UdevRules *rules);
diff --git a/src/udev/udevadm-verify.c b/src/udev/udevadm-verify.c
new file mode 100644 (file)
index 0000000..8819b07
--- /dev/null
@@ -0,0 +1,125 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "pretty-print.h"
+#include "strv.h"
+#include "udev-rules.h"
+#include "udevadm.h"
+
+static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY;
+
+static int help(void) {
+        _cleanup_free_ char *link = NULL;
+        int r;
+
+        r = terminal_urlify_man("udevadm", "8", &link);
+        if (r < 0)
+                return log_oom();
+
+        printf("%s verify [OPTIONS] FILE...\n"
+               "\n%sVerify udev rules files.%s\n\n"
+               "  -h --help                            Show this help\n"
+               "  -V --version                         Show package version\n"
+               "  -N --resolve-names=early|never       When to resolve names\n"
+               "\nSee the %s for details.\n",
+               program_invocation_short_name,
+               ansi_highlight(),
+               ansi_normal(),
+               link);
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+        static const struct option options[] = {
+                { "help",          no_argument,       NULL, 'h' },
+                { "version",       no_argument,       NULL, 'V' },
+                { "resolve-names", required_argument, NULL, 'N' },
+                {}
+        };
+
+        int c;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "hVN:", options, NULL)) >= 0)
+                switch (c) {
+                case 'h':
+                        return help();
+                case 'V':
+                        return print_version();
+                case 'N':
+                        arg_resolve_name_timing = resolve_name_timing_from_string(optarg);
+                        if (arg_resolve_name_timing < 0)
+                                return log_error_errno(arg_resolve_name_timing,
+                                                       "--resolve-names= takes \"early\" or \"never\"");
+                        /*
+                         * In the verifier "late" has the effect of "never",
+                         * and "never" would generate irrelevant diagnostics,
+                         * so map "never" to "late".
+                         */
+                        if (arg_resolve_name_timing == RESOLVE_NAME_NEVER)
+                                arg_resolve_name_timing = RESOLVE_NAME_LATE;
+                        break;
+                case '?':
+                        return -EINVAL;
+                default:
+                        assert_not_reached();
+                }
+
+        if (optind == argc)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No rules file specified.");
+
+        return 1;
+}
+
+static int verify_rules_file(UdevRules *rules, const char *fname) {
+        int r;
+
+        r = udev_rules_parse_file(rules, fname);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse rules file %s: %m", fname);
+
+        unsigned issues = udev_check_current_rule_file(rules);
+        unsigned mask = (1U << LOG_ERR) | (1U << LOG_WARNING);
+        if (issues & mask)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "%s: udev rules check failed", fname);
+
+        return 0;
+}
+
+static int verify_rules(UdevRules *rules, char **files) {
+        int r, rv = 0;
+
+        STRV_FOREACH(fp, files) {
+                r = verify_rules_file(rules, *fp);
+                if (r < 0 && rv >= 0)
+                        rv = r;
+        }
+
+        return rv;
+}
+
+int verify_main(int argc, char *argv[], void *userdata) {
+        _cleanup_(udev_rules_freep) UdevRules *rules = NULL;
+        int r;
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                return r;
+
+        rules = udev_rules_new(arg_resolve_name_timing);
+        if (!rules)
+                return -ENOMEM;
+
+        return verify_rules(rules, strv_skip(argv, optind));
+}
index b742c1a28730deda43e6ba07e68dd1d29013a485..273d3c5b294f29f668cbebca977ebad3872100eb 100644 (file)
@@ -25,6 +25,7 @@ static int help(void) {
                 { "monitor",      "Listen to kernel and udev events"  },
                 { "test",         "Test an event run"                 },
                 { "test-builtin", "Test a built-in command"           },
+                { "verify",       "Verify udev rules files"           },
                 { "wait",         "Wait for device or device symlink" },
                 { "lock",         "Lock a block device"               },
         };
@@ -104,6 +105,7 @@ static int udevadm_main(int argc, char *argv[]) {
                 { "test-builtin", VERB_ANY, VERB_ANY, 0, builtin_main },
                 { "wait",         VERB_ANY, VERB_ANY, 0, wait_main    },
                 { "lock",         VERB_ANY, VERB_ANY, 0, lock_main    },
+                { "verify",       VERB_ANY, VERB_ANY, 0, verify_main  },
                 { "version",      VERB_ANY, VERB_ANY, 0, version_main },
                 { "help",         VERB_ANY, VERB_ANY, 0, help_main    },
                 {}
index 417611affe30b7823e5ccff0a1dbe2ed06180768..7920a70d5b1812788f460f75cf958543e7ca2b67 100644 (file)
@@ -13,6 +13,7 @@ int monitor_main(int argc, char *argv[], void *userdata);
 int hwdb_main(int argc, char *argv[], void *userdata);
 int test_main(int argc, char *argv[], void *userdata);
 int builtin_main(int argc, char *argv[], void *userdata);
+int verify_main(int argc, char *argv[], void *userdata);
 int wait_main(int argc, char *argv[], void *userdata);
 int lock_main(int argc, char *argv[], void *userdata);