diff --git a/NEWS b/NEWS index 685a894d3a0..f6f49cb9020 100644 --- a/NEWS +++ b/NEWS @@ -44,6 +44,12 @@ CHANGES WITH 252 in spe: * C.UTF-8 is used as the default locale if nothing else has been configured. + * Extend [Condition|Assert]Firmware= to conditionalize on certain SMBIOS + fields. For example + ConditionFirmware=smbios-field(board_name = "Custom Board") will + conditionalize a unit so that it is only run when + /sys/class/dmi/id/board_name contains "Custom Board" (without quotes). + Changes in sd-boot, bootctl, and the Boot Loader Specification: * The Boot Loader Specification has been cleaned up and clarified. diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index 95f1b98cbd1..a9114fb353f 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -1235,10 +1235,24 @@ ConditionFirmware= - Check whether the system's firmware is of a certain type. Possible values are: - uefi (for systems with EFI), - device-tree (for systems with a device tree) and - device-tree-compatible(xyz) (for systems with a device tree that is compatible to xyz). + Check whether the system's firmware is of a certain type. Multiple values are possible. + + uefi for systems with EFI. + + device-tree for systems with a device tree. + + device-tree-compatible(value) for systems with a device tree that is compatible to + value. + + smbios-field(field operator value) + for systems with a SMBIOS field containing a certain value. + field is the name of the SMBIOS field exposed as sysfs attribute file + below /sys/class/dmi/id/. + operator is one of <, <=, + >=, >, =, != for version + comparison, or =$, !=$ for string comparison. + value is the expected value of the SMBIOS field (shell-style globs are possible if + =$ or!=$ is used). diff --git a/src/shared/condition.c b/src/shared/condition.c index 7a983edfd7a..3a09b493fc3 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include #include @@ -184,6 +185,10 @@ static int condition_test_credential(Condition *c, char **env) { typedef enum { /* Listed in order of checking. Note that some comparators are prefixes of others, hence the longest * should be listed first. */ + _ORDER_FNMATCH_FIRST, + ORDER_FNMATCH_EQUAL = _ORDER_FNMATCH_FIRST, + ORDER_FNMATCH_UNEQUAL, + _ORDER_FNMATCH_LAST = ORDER_FNMATCH_UNEQUAL, ORDER_LOWER_OR_EQUAL, ORDER_GREATER_OR_EQUAL, ORDER_LOWER, @@ -194,8 +199,10 @@ typedef enum { _ORDER_INVALID = -EINVAL, } OrderOperator; -static OrderOperator parse_order(const char **s) { +static OrderOperator parse_order(const char **s, bool allow_fnmatch) { static const char *const prefix[_ORDER_MAX] = { + [ORDER_FNMATCH_EQUAL] = "=$", + [ORDER_FNMATCH_UNEQUAL] = "!=$", [ORDER_LOWER_OR_EQUAL] = "<=", [ORDER_GREATER_OR_EQUAL] = ">=", [ORDER_LOWER] = "<", @@ -209,6 +216,8 @@ static OrderOperator parse_order(const char **s) { e = startswith(*s, prefix[i]); if (e) { + if (!allow_fnmatch && (i >= _ORDER_FNMATCH_FIRST && i <= _ORDER_FNMATCH_LAST)) + break; *s = e; return i; } @@ -268,7 +277,7 @@ static int condition_test_kernel_version(Condition *c, char **env) { break; s = strstrip(word); - order = parse_order(&s); + order = parse_order(&s, /* allow_fnmatch= */ false); if (order >= 0) { s += strspn(s, WHITESPACE); if (isempty(s)) { @@ -329,7 +338,7 @@ static int condition_test_osrelease(Condition *c, char **env) { "Failed to parse parameter, key/value format expected: %m"); /* Do not allow whitespace after the separator, as that's not a valid os-release format */ - order = parse_order(&word); + order = parse_order(&word, /* allow_fnmatch= */ false); if (order < 0 || isempty(word) || strchr(WHITESPACE, *word) != NULL) return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse parameter, key/value format expected: %m"); @@ -366,7 +375,7 @@ static int condition_test_memory(Condition *c, char **env) { m = physical_memory(); p = c->parameter; - order = parse_order(&p); + order = parse_order(&p, /* allow_fnmatch= */ false); if (order < 0) order = ORDER_GREATER_OR_EQUAL; /* default to >= check, if nothing is specified. */ @@ -392,7 +401,7 @@ static int condition_test_cpus(Condition *c, char **env) { return log_debug_errno(n, "Failed to determine CPUs in affinity mask: %m"); p = c->parameter; - order = parse_order(&p); + order = parse_order(&p, /* allow_fnmatch= */ false); if (order < 0) order = ORDER_GREATER_OR_EQUAL; /* default to >= check, if nothing is specified. */ @@ -578,8 +587,62 @@ static int condition_test_firmware_devicetree_compatible(const char *dtcarg) { return strv_contains(dtcompatlist, dtcarg); } +static int condition_test_firmware_smbios_field(const char *expression) { + _cleanup_free_ char *field = NULL, *expected_value = NULL, *actual_value = NULL; + OrderOperator operator; + int r; + + assert(expression); + + /* Parse SMBIOS field */ + r = extract_first_word(&expression, &field, "!<=>$", EXTRACT_RETAIN_SEPARATORS); + if (r < 0) + return r; + if (r == 0 || isempty(expression)) + return -EINVAL; + + /* Remove trailing spaces from SMBIOS field */ + delete_trailing_chars(field, WHITESPACE); + + /* Parse operator */ + operator = parse_order(&expression, /* allow_fnmatch= */ true); + if (operator < 0) + return operator; + + /* Parse expected value */ + r = extract_first_word(&expression, &expected_value, NULL, EXTRACT_UNQUOTE); + if (r < 0) + return r; + if (r == 0 || !isempty(expression)) + return -EINVAL; + + /* Read actual value from sysfs */ + if (!filename_is_valid(field)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid SMBIOS field name"); + + const char *p = strjoina("/sys/class/dmi/id/", field); + r = read_virtual_file(p, SIZE_MAX, &actual_value, NULL); + if (r < 0) { + log_debug_errno(r, "Failed to read %s: %m", p); + if (r == -ENOENT) + return false; + return r; + } + + /* Remove trailing newline */ + delete_trailing_chars(actual_value, WHITESPACE); + + /* Finally compare actual and expected value */ + if (operator == ORDER_FNMATCH_EQUAL) + return fnmatch(expected_value, actual_value, FNM_EXTMATCH) != FNM_NOMATCH; + if (operator == ORDER_FNMATCH_UNEQUAL) + return fnmatch(expected_value, actual_value, FNM_EXTMATCH) == FNM_NOMATCH; + return test_order(strverscmp_improved(actual_value, expected_value), operator); +} + static int condition_test_firmware(Condition *c, char **env) { - sd_char *dtc; + sd_char *arg; + int r; assert(c); assert(c->parameter); @@ -592,24 +655,40 @@ static int condition_test_firmware(Condition *c, char **env) { return false; } else return true; - } else if ((dtc = startswith(c->parameter, "device-tree-compatible("))) { - _cleanup_free_ char *dtcarg = NULL; + } else if ((arg = startswith(c->parameter, "device-tree-compatible("))) { + _cleanup_free_ char *dtc_arg = NULL; char *end; - end = strchr(dtc, ')'); + end = strchr(arg, ')'); if (!end || *(end + 1) != '\0') { - log_debug("Malformed Firmware condition \"%s\"", c->parameter); + log_debug("Malformed ConditionFirmware=%s", c->parameter); return false; } - dtcarg = strndup(dtc, end - dtc); - if (!dtcarg) + dtc_arg = strndup(arg, end - arg); + if (!dtc_arg) return -ENOMEM; - return condition_test_firmware_devicetree_compatible(dtcarg); + return condition_test_firmware_devicetree_compatible(dtc_arg); } else if (streq(c->parameter, "uefi")) return is_efi_boot(); - else { + else if ((arg = startswith(c->parameter, "smbios-field("))) { + _cleanup_free_ char *smbios_arg = NULL; + char *end; + + end = strchr(arg, ')'); + if (!end || *(end + 1) != '\0') + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Malformed ConditionFirmware=%s: %m", c->parameter); + + smbios_arg = strndup(arg, end - arg); + if (!smbios_arg) + return log_oom_debug(); + + r = condition_test_firmware_smbios_field(smbios_arg); + if (r < 0) + return log_debug_errno(r, "Malformed ConditionFirmware=%s: %m", c->parameter); + return r; + } else { log_debug("Unsupported Firmware condition \"%s\"", c->parameter); return false; } diff --git a/src/test/test-condition.c b/src/test/test-condition.c index 56b5ad88a25..6cb889c8d69 100644 --- a/src/test/test-condition.c +++ b/src/test/test-condition.c @@ -17,6 +17,7 @@ #include "efi-loader.h" #include "env-util.h" #include "errno-util.h" +#include "fileio.h" #include "fs-util.h" #include "hostname-util.h" #include "id128-util.h" @@ -305,6 +306,136 @@ TEST(condition_test_architecture) { condition_free(condition); } +TEST(condition_test_firmware_smbios_field) { + _cleanup_free_ char *bios_vendor = NULL, *bios_version = NULL; + const char *expression; + Condition *condition; + + /* Test some malformed smbios-field arguments */ + condition = condition_new(CONDITION_FIRMWARE, "smbios-field()", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed=)", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed=)", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_FIRMWARE, "smbios-field(not_existing=nothing garbage)", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + /* Test not existing SMBIOS field */ + condition = condition_new(CONDITION_FIRMWARE, "smbios-field(not_existing=nothing)", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + /* Test with bios_vendor, if available */ + if (read_virtual_file("/sys/class/dmi/id/bios_vendor", SIZE_MAX, &bios_vendor, NULL) <= 0) + return; + + /* remove trailing newline */ + strstrip(bios_vendor); + + /* Check if the bios_vendor contains any spaces we should quote */ + const char *quote = strchr(bios_vendor, ' ') ? "\"" : ""; + + /* Test equality / inequality using fnmatch() */ + expression = strjoina("smbios-field(bios_vendor =$ ", quote, bios_vendor, quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + expression = strjoina("smbios-field(bios_vendor=$", quote, bios_vendor, quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + expression = strjoina("smbios-field(bios_vendor !=$ ", quote, bios_vendor, quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + expression = strjoina("smbios-field(bios_vendor!=$", quote, bios_vendor, quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + expression = strjoina("smbios-field(bios_vendor =$ ", quote, bios_vendor, "*", quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + /* Test version comparison with bios_version, if available */ + if (read_virtual_file("/sys/class/dmi/id/bios_version", SIZE_MAX, &bios_version, NULL) <= 0) + return; + + /* remove trailing newline */ + strstrip(bios_version); + + /* Check if the bios_version contains any spaces we should quote */ + quote = strchr(bios_version, ' ') ? "\"" : ""; + + expression = strjoina("smbios-field(bios_version = ", quote, bios_version, quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + expression = strjoina("smbios-field(bios_version != ", quote, bios_version, quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + expression = strjoina("smbios-field(bios_version <= ", quote, bios_version, quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + expression = strjoina("smbios-field(bios_version >= ", quote, bios_version, quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + expression = strjoina("smbios-field(bios_version < ", quote, bios_version, ".1", quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + expression = strjoina("smbios-field(bios_version > ", quote, bios_version, ".1", quote, ")"); + condition = condition_new(CONDITION_FIRMWARE, expression, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); +} + TEST(condition_test_kernel_command_line) { Condition *condition; int r; @@ -427,7 +558,7 @@ TEST(condition_test_kernel_version) { assert_se(condition_test(condition, environ) == 0); condition_free(condition); - condition = condition_new(CONDITION_KERNEL_VERSION, ">= 4711.8.15", false, false); + condition = condition_new(CONDITION_KERNEL_VERSION, " >= 4711.8.15", false, false); assert_se(condition); assert_se(condition_test(condition, environ) == 0); condition_free(condition); @@ -1042,6 +1173,19 @@ TEST(condition_test_os_release) { assert_se(condition_test(condition, environ) == 0); condition_free(condition); + /* Test fnmatch() operators */ + key_value_pair = strjoina(os_release_pairs[0], "=$", quote, os_release_pairs[1], quote); + condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + key_value_pair = strjoina(os_release_pairs[0], "!=$", quote, os_release_pairs[1], quote); + condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + /* Some distros (eg: Arch) do not set VERSION_ID */ if (parse_os_release(NULL, "VERSION_ID", &version_id) <= 0) return;