From cbcdcaaa0ec5eccfe90e51391f9b8d027c555abf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig?= <u.kleine-koenig@pengutronix.de>
Date: Fri, 9 Oct 2020 12:13:00 +0200
Subject: [PATCH] Add support for conditions on the machines firmware

This allows to limit units to machines that run on a certain firmware
type. For device tree defined machines checking against the machine's
compatible is also possible.
---
 docs/TRANSIENT-SETTINGS.md                    |  1 +
 man/systemd.link.xml                          | 12 +++
 man/systemd.netdev.xml                        | 11 +++
 man/systemd.unit.xml                          | 10 +++
 src/core/load-fragment-gperf.gperf.m4         |  1 +
 src/network/netdev/netdev-gperf.gperf         |  1 +
 src/network/networkd-network-gperf.gperf      |  1 +
 src/shared/condition.c                        | 79 +++++++++++++++++++
 src/shared/condition.h                        |  1 +
 .../fuzz/fuzz-netdev-parser/directives.netdev |  1 +
 .../fuzz-network-parser/directives.network    |  1 +
 test/fuzz/fuzz-unit-file/directives.service   |  1 +
 12 files changed, 120 insertions(+)

diff --git a/docs/TRANSIENT-SETTINGS.md b/docs/TRANSIENT-SETTINGS.md
index 9fa856ec21..070ad5a285 100644
--- a/docs/TRANSIENT-SETTINGS.md
+++ b/docs/TRANSIENT-SETTINGS.md
@@ -68,6 +68,7 @@ Most generic unit settings are available for transient units.
 ✓ ConditionKernelCommandLine=
 ✓ ConditionKernelVersion=
 ✓ ConditionArchitecture=
+✓ ConditionFirmware=
 ✓ ConditionVirtualization=
 ✓ ConditionSecurity=
 ✓ ConditionCapability=
diff --git a/man/systemd.link.xml b/man/systemd.link.xml
index 5918a32189..6be115026b 100644
--- a/man/systemd.link.xml
+++ b/man/systemd.link.xml
@@ -222,6 +222,18 @@
           </para>
         </listitem>
       </varlistentry>
+
+      <varlistentry id='firmware'>
+        <term><varname>Firmware=</varname></term>
+        <listitem>
+          <para>Checks whether the system is running on a machine with the specified firmware. See
+          <varname>ConditionFirmware=</varname> in
+          <citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+          for details. When prefixed with an exclamation mark (<literal>!</literal>), the result is negated.
+          If an empty string is assigned, then previously assigned value is cleared.
+          </para>
+        </listitem>
+      </varlistentry>
     </variablelist>
 
   </refsect1>
diff --git a/man/systemd.netdev.xml b/man/systemd.netdev.xml
index c1b1980bac..d93ee8bb30 100644
--- a/man/systemd.netdev.xml
+++ b/man/systemd.netdev.xml
@@ -258,6 +258,17 @@
           </para>
         </listitem>
       </varlistentry>
+      <varlistentry>
+        <term><varname>Firmware=</varname></term>
+        <listitem>
+          <para>Checks whether the system is running on a machine with the specified firmware. See
+          <literal>ConditionFirmware=</literal> in
+          <citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+          for details. When prefixed with an exclamation mark (<literal>!</literal>), the result is negated.
+          If an empty string is assigned, then previously assigned value is cleared.
+          </para>
+        </listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml
index 631658d88a..7f37f01ef9 100644
--- a/man/systemd.unit.xml
+++ b/man/systemd.unit.xml
@@ -1172,6 +1172,16 @@
           </listitem>
         </varlistentry>
 
+        <varlistentry>
+          <term><varname>ConditionFirmware=</varname></term>
+
+          <listitem><para>Check whether the system's firmware is of a certain type. Possible values are:
+          <literal>uefi</literal> (for systems with EFI),
+          <literal>device-tree</literal> (for systems with a device tree) and
+          <literal>device-tree-compatible(xyz)</literal> (for systems with a device tree that is compatible to <literal>xyz</literal>).</para>
+          </listitem>
+        </varlistentry>
+
         <varlistentry>
           <term><varname>ConditionVirtualization=</varname></term>
 
diff --git a/src/core/load-fragment-gperf.gperf.m4 b/src/core/load-fragment-gperf.gperf.m4
index c531380401..7c3c1c9095 100644
--- a/src/core/load-fragment-gperf.gperf.m4
+++ b/src/core/load-fragment-gperf.gperf.m4
@@ -297,6 +297,7 @@ Unit.ConditionFileIsExecutable,          config_parse_unit_condition_path,
 Unit.ConditionNeedsUpdate,               config_parse_unit_condition_path,            CONDITION_NEEDS_UPDATE,             offsetof(Unit, conditions)
 Unit.ConditionFirstBoot,                 config_parse_unit_condition_string,          CONDITION_FIRST_BOOT,               offsetof(Unit, conditions)
 Unit.ConditionArchitecture,              config_parse_unit_condition_string,          CONDITION_ARCHITECTURE,             offsetof(Unit, conditions)
+Unit.ConditionFirmware,                  config_parse_unit_condition_string,          CONDITION_FIRMWARE,                 offsetof(Unit, conditions)
 Unit.ConditionVirtualization,            config_parse_unit_condition_string,          CONDITION_VIRTUALIZATION,           offsetof(Unit, conditions)
 Unit.ConditionHost,                      config_parse_unit_condition_string,          CONDITION_HOST,                     offsetof(Unit, conditions)
 Unit.ConditionKernelCommandLine,         config_parse_unit_condition_string,          CONDITION_KERNEL_COMMAND_LINE,      offsetof(Unit, conditions)
diff --git a/src/network/netdev/netdev-gperf.gperf b/src/network/netdev/netdev-gperf.gperf
index 8abe044890..6381bcfe0c 100644
--- a/src/network/netdev/netdev-gperf.gperf
+++ b/src/network/netdev/netdev-gperf.gperf
@@ -44,6 +44,7 @@ Match.Virtualization,                     config_parse_net_condition,
 Match.KernelCommandLine,                  config_parse_net_condition,                CONDITION_KERNEL_COMMAND_LINE, offsetof(NetDev, conditions)
 Match.KernelVersion,                      config_parse_net_condition,                CONDITION_KERNEL_VERSION,      offsetof(NetDev, conditions)
 Match.Architecture,                       config_parse_net_condition,                CONDITION_ARCHITECTURE,        offsetof(NetDev, conditions)
+Match.Firmware,                           config_parse_net_condition,                CONDITION_FIRMWARE,            offsetof(NetDev, conditions)
 NetDev.Description,                       config_parse_string,                       0,                             offsetof(NetDev, description)
 NetDev.Name,                              config_parse_ifname,                       0,                             offsetof(NetDev, ifname)
 NetDev.Kind,                              config_parse_netdev_kind,                  0,                             offsetof(NetDev, kind)
diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf
index 19d52a9296..91c21525fb 100644
--- a/src/network/networkd-network-gperf.gperf
+++ b/src/network/networkd-network-gperf.gperf
@@ -56,6 +56,7 @@ Match.Virtualization,                        config_parse_net_condition,
 Match.KernelCommandLine,                     config_parse_net_condition,                               CONDITION_KERNEL_COMMAND_LINE, offsetof(Network, conditions)
 Match.KernelVersion,                         config_parse_net_condition,                               CONDITION_KERNEL_VERSION,      offsetof(Network, conditions)
 Match.Architecture,                          config_parse_net_condition,                               CONDITION_ARCHITECTURE,        offsetof(Network, conditions)
+Match.Firmware,                              config_parse_net_condition,                               CONDITION_FIRMWARE,            offsetof(Network, conditions)
 Link.MACAddress,                             config_parse_hwaddr,                                      0,                             offsetof(Network, mac)
 Link.MTUBytes,                               config_parse_mtu,                                         AF_UNSPEC,                     offsetof(Network, mtu)
 Link.Group,                                  config_parse_uint32,                                      0,                             offsetof(Network, group)
diff --git a/src/shared/condition.c b/src/shared/condition.c
index d84377c269..d4d9eacc4b 100644
--- a/src/shared/condition.c
+++ b/src/shared/condition.c
@@ -5,6 +5,7 @@
 #include <fnmatch.h>
 #include <limits.h>
 #include <stdlib.h>
+#include <sys/stat.h>
 #include <sys/types.h>
 #include <sys/utsname.h>
 #include <time.h>
@@ -450,6 +451,81 @@ static int condition_test_architecture(Condition *c, char **env) {
         return a == b;
 }
 
+#define DTCOMPAT_FILE "/sys/firmware/devicetree/base/compatible"
+static int condition_test_firmware_devicetree_compatible(const char *dtcarg) {
+        int r;
+        _cleanup_free_ char *dtcompat = NULL;
+        _cleanup_strv_free_ char **dtcompatlist = NULL;
+        size_t size;
+
+        r = read_full_virtual_file(DTCOMPAT_FILE, &dtcompat, &size);
+        if (r < 0) {
+                /* if the path doesn't exist it is incompatible */
+                if (r != -ENOENT)
+                        log_debug_errno(r, "Failed to open() '%s', assuming machine is incompatible: %m", DTCOMPAT_FILE);
+                return false;
+        }
+
+        /* Not sure this can happen, but play safe. */
+        if (size == 0) {
+                log_debug("%s has zero length, assuming machine is incompatible", DTCOMPAT_FILE);
+                return false;
+        }
+
+        /*
+         * /sys/firmware/devicetree/base/compatible consists of one or more
+         * strings, each ending in '\0'. So the last character in dtcompat must
+         * be a '\0'.
+         */
+        if (dtcompat[size - 1] != '\0') {
+                log_debug("%s is in an unknown format, assuming machine is incompatible", DTCOMPAT_FILE);
+                return false;
+        }
+
+        dtcompatlist = strv_parse_nulstr(dtcompat, size);
+        if (!dtcompatlist)
+                return -ENOMEM;
+
+        return strv_contains(dtcompatlist, dtcarg);
+}
+
+static int condition_test_firmware(Condition *c, char **env) {
+        sd_char *dtc;
+
+        assert(c);
+        assert(c->parameter);
+        assert(c->type == CONDITION_FIRMWARE);
+
+        if (streq(c->parameter, "device-tree")) {
+                if (access("/sys/firmware/device-tree/", F_OK) < 0) {
+                        if (errno != ENOENT)
+                                log_debug_errno(errno, "Unexpected error when checking for /sys/firmware/device-tree/: %m");
+                        return false;
+                } else
+                        return true;
+        } else if ((dtc = startswith(c->parameter, "device-tree-compatible("))) {
+                _cleanup_free_ char *dtcarg = NULL;
+                char *end;
+
+                end = strchr(dtc, ')');
+                if (!end || *(end + 1) != '\0') {
+                        log_debug("Malformed Firmware condition \"%s\"", c->parameter);
+                        return false;
+                }
+
+                dtcarg = strndup(dtc, end - dtc);
+                if (!dtcarg)
+                        return -ENOMEM;
+
+                return condition_test_firmware_devicetree_compatible(dtcarg);
+        } else if (streq(c->parameter, "uefi"))
+                return is_efi_boot();
+        else {
+                log_debug("Unsupported Firmware condition \"%s\"", c->parameter);
+                return false;
+        }
+}
+
 static int condition_test_host(Condition *c, char **env) {
         _cleanup_free_ char *h = NULL;
         sd_id128_t x, y;
@@ -843,6 +919,7 @@ int condition_test(Condition *c, char **env) {
                 [CONDITION_HOST]                     = condition_test_host,
                 [CONDITION_AC_POWER]                 = condition_test_ac_power,
                 [CONDITION_ARCHITECTURE]             = condition_test_architecture,
+                [CONDITION_FIRMWARE]                 = condition_test_firmware,
                 [CONDITION_NEEDS_UPDATE]             = condition_test_needs_update,
                 [CONDITION_FIRST_BOOT]               = condition_test_first_boot,
                 [CONDITION_USER]                     = condition_test_user,
@@ -949,6 +1026,7 @@ void condition_dump_list(Condition *first, FILE *f, const char *prefix, conditio
 
 static const char* const condition_type_table[_CONDITION_TYPE_MAX] = {
         [CONDITION_ARCHITECTURE] = "ConditionArchitecture",
+        [CONDITION_FIRMWARE] = "ConditionFirmware",
         [CONDITION_VIRTUALIZATION] = "ConditionVirtualization",
         [CONDITION_HOST] = "ConditionHost",
         [CONDITION_KERNEL_COMMAND_LINE] = "ConditionKernelCommandLine",
@@ -981,6 +1059,7 @@ DEFINE_STRING_TABLE_LOOKUP(condition_type, ConditionType);
 
 static const char* const assert_type_table[_CONDITION_TYPE_MAX] = {
         [CONDITION_ARCHITECTURE] = "AssertArchitecture",
+        [CONDITION_FIRMWARE] = "AssertFirmware",
         [CONDITION_VIRTUALIZATION] = "AssertVirtualization",
         [CONDITION_HOST] = "AssertHost",
         [CONDITION_KERNEL_COMMAND_LINE] = "AssertKernelCommandLine",
diff --git a/src/shared/condition.h b/src/shared/condition.h
index 75c430e9e0..6678689d7c 100644
--- a/src/shared/condition.h
+++ b/src/shared/condition.h
@@ -9,6 +9,7 @@
 
 typedef enum ConditionType {
         CONDITION_ARCHITECTURE,
+        CONDITION_FIRMWARE,
         CONDITION_VIRTUALIZATION,
         CONDITION_HOST,
         CONDITION_KERNEL_COMMAND_LINE,
diff --git a/test/fuzz/fuzz-netdev-parser/directives.netdev b/test/fuzz/fuzz-netdev-parser/directives.netdev
index d4e5598409..8373ac7e89 100644
--- a/test/fuzz/fuzz-netdev-parser/directives.netdev
+++ b/test/fuzz/fuzz-netdev-parser/directives.netdev
@@ -22,6 +22,7 @@ Mode=
 SourceMACAddress=
 [Match]
 Architecture=
+Firmware=
 Host=
 KernelVersion=
 Virtualization=
diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network
index d6c1cc7f92..096ba9a8a9 100644
--- a/test/fuzz/fuzz-network-parser/directives.network
+++ b/test/fuzz/fuzz-network-parser/directives.network
@@ -18,6 +18,7 @@ KernelVersion=
 Type=
 Driver=
 Architecture=
+Firmware=
 Path=
 WLANInterfaceType=
 SSID=
diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service
index 9fc83f04d2..73e1707c5c 100644
--- a/test/fuzz/fuzz-unit-file/directives.service
+++ b/test/fuzz/fuzz-unit-file/directives.service
@@ -36,6 +36,7 @@ CollectMode=
 ConditionACPower=
 ConditionArchitecture=
 ConditionCPUs=
+ConditionFirmware=
 ConditionCapability=
 ConditionControlGroupController=
 ConditionDirectoryNotEmpty=