From c32bd8b0dc9796291842fea3491f03544497d817 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Mon, 22 Jun 2026 13:45:05 +0100 Subject: [PATCH 01/71] core: add all manager timestamps to metrics report These are all very useful for establishing the health of a fleet, so export them too Follow-up for 0b0db27050595251b40b4e7cf56593a275eaf3c2 --- src/core/varlink-metrics.c | 298 +++++++++++++++++++------ src/shared/metrics.c | 22 +- src/shared/metrics.h | 1 + test/units/TEST-74-AUX-UTILS.report.sh | 11 + 4 files changed, 253 insertions(+), 79 deletions(-) diff --git a/src/core/varlink-metrics.c b/src/core/varlink-metrics.c index 0e6bfa961890d..5232da633c0cd 100644 --- a/src/core/varlink-metrics.c +++ b/src/core/varlink-metrics.c @@ -98,57 +98,231 @@ static int version_build_json(const MetricFamily *mf, sd_varlink *vl, void *user /* fields= */ NULL); } -static int boot_timestamp_build_json( - const MetricFamily *mf, - sd_varlink *vl, - const dual_timestamp *t, - bool with_monotonic) { +/* Single source of truth for all manager timestamp metrics, driving both List (the values) and Describe + * (the schema advertised below). The .description is prefixed with "CLOCK_REALTIME "/"CLOCK_MONOTONIC " on + * emission; firmware, loader and kernel have no meaningful monotonic value (a pre-kernel offset or zero), + * hence they only expose the .Realtime metric (with_monotonic=false). */ +static const struct { + const char *name; + bool with_monotonic; + const char *description; +} manager_timestamp_metrics[_MANAGER_TIMESTAMP_MAX] = { + [MANAGER_TIMESTAMP_FIRMWARE] = { + .name = "FirmwareTimestamp", + .with_monotonic = false, + .description = "microseconds at which the firmware began execution (CLOCK_MONOTONIC is a pre-kernel offset, not reported)", + }, + [MANAGER_TIMESTAMP_LOADER] = { + .name = "LoaderTimestamp", + .with_monotonic = false, + .description = "microseconds at which the boot loader began execution (CLOCK_MONOTONIC is a pre-kernel offset, not reported)", + }, + [MANAGER_TIMESTAMP_KERNEL] = { + .name = "KernelTimestamp", + .with_monotonic = false, + .description = "microseconds at which the kernel started (CLOCK_MONOTONIC == 0)", + }, + [MANAGER_TIMESTAMP_INITRD] = { + .name = "InitRDTimestamp", + .with_monotonic = true, + .description = "microseconds at which the initrd began execution", + }, + [MANAGER_TIMESTAMP_USERSPACE] = { + .name = "UserspaceTimestamp", + .with_monotonic = true, + .description = "microseconds at which userspace was reached", + }, + [MANAGER_TIMESTAMP_FINISH] = { + .name = "FinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which userspace finished booting", + }, + [MANAGER_TIMESTAMP_SECURITY_START] = { + .name = "SecurityStartTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager started uploading security policies to the kernel", + }, + [MANAGER_TIMESTAMP_SECURITY_FINISH] = { + .name = "SecurityFinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager finished uploading security policies to the kernel", + }, + [MANAGER_TIMESTAMP_GENERATORS_START] = { + .name = "GeneratorsStartTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager started executing generators", + }, + [MANAGER_TIMESTAMP_GENERATORS_FINISH] = { + .name = "GeneratorsFinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager finished executing generators", + }, + [MANAGER_TIMESTAMP_UNITS_LOAD_START] = { + .name = "UnitsLoadStartTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager first started loading units", + }, + [MANAGER_TIMESTAMP_UNITS_LOAD_FINISH] = { + .name = "UnitsLoadFinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager first finished loading units", + }, + [MANAGER_TIMESTAMP_UNITS_LOAD] = { + .name = "UnitsLoadTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager last started loading units", + }, + [MANAGER_TIMESTAMP_INITRD_SECURITY_START] = { + .name = "InitRDSecurityStartTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager started uploading security policies to the kernel in the initrd", + }, + [MANAGER_TIMESTAMP_INITRD_SECURITY_FINISH] = { + .name = "InitRDSecurityFinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager finished uploading security policies to the kernel in the initrd", + }, + [MANAGER_TIMESTAMP_INITRD_GENERATORS_START] = { + .name = "InitRDGeneratorsStartTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager started executing generators in the initrd", + }, + [MANAGER_TIMESTAMP_INITRD_GENERATORS_FINISH] = { + .name = "InitRDGeneratorsFinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager finished executing generators in the initrd", + }, + [MANAGER_TIMESTAMP_INITRD_UNITS_LOAD_START] = { + .name = "InitRDUnitsLoadStartTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager first started loading units in the initrd", + }, + [MANAGER_TIMESTAMP_INITRD_UNITS_LOAD_FINISH] = { + .name = "InitRDUnitsLoadFinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which the manager first finished loading units in the initrd", + }, + [MANAGER_TIMESTAMP_SHUTDOWN_START] = { + .name = "ShutdownStartTimestamp", + .with_monotonic = true, + .description = "microseconds at which shutdown began, i.e. units started to be stopped", + }, + [MANAGER_TIMESTAMP_SHUTDOWN_FINISH] = { + .name = "ShutdownFinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which all units finished stopping during shutdown", + }, + [MANAGER_TIMESTAMP_PREVIOUS_SHUTDOWN_START] = { + .name = "PreviousShutdownStartTimestamp", + .with_monotonic = true, + .description = "microseconds at which shutdown began during the previous boot, i.e. units started to be stopped, if available (e.g.: kexec or soft-reboot)", + }, + [MANAGER_TIMESTAMP_PREVIOUS_SHUTDOWN_FINISH] = { + .name = "PreviousShutdownFinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which all units finished stopping during the shutdown of the previous boot, if available (e.g.: kexec or soft-reboot)", + }, + [MANAGER_TIMESTAMP_PREVIOUS_SHUTDOWN_LATE_START] = { + .name = "PreviousShutdownLateStartTimestamp", + .with_monotonic = true, + .description = "microseconds at which systemd-shutdown began execution during the previous boot, restored from the LUO payload after a kexec-based live update", + }, + [MANAGER_TIMESTAMP_PREVIOUS_SHUTDOWN_LATE_FINISH] = { + .name = "PreviousShutdownLateFinishTimestamp", + .with_monotonic = true, + .description = "microseconds at which systemd-shutdown was about to kexec into the current kernel during the previous boot, restored from the LUO payload after a kexec-based live update", + }, +}; +static int manager_timestamps_build_json(sd_varlink *vl, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); int r; - assert(mf && mf->name); assert(vl); - assert(t); - if (timestamp_is_set(t->realtime)) { - r = metric_build_send_unsigned( - mf, /* the .Realtime metric family entry */ - vl, - /* object= */ NULL, - t->realtime, - /* fields= */ NULL); - if (r < 0) - return r; - } + FOREACH_ELEMENT(i, manager_timestamp_metrics) { + if (!i->name) + continue; - if (with_monotonic && timestamp_is_set(t->monotonic)) { - assert(endswith(mf[1].name, ".Monotonic")); - r = metric_build_send_unsigned( - mf + 1, /* the .Monotonic sibling is the next entry */ - vl, - /* object= */ NULL, - t->monotonic, - /* fields= */ NULL); - if (r < 0) - return r; + const dual_timestamp *t = manager->timestamps + (i - manager_timestamp_metrics); + + if (timestamp_is_set(t->realtime)) { + _cleanup_free_ char *name = strjoin(METRIC_IO_SYSTEMD_MANAGER_PREFIX, i->name, ".Realtime"); + if (!name) + return -ENOMEM; + + r = metric_build_send_unsigned( + &(const MetricFamily) { .name = name }, + vl, + /* object= */ NULL, + t->realtime, + /* fields= */ NULL); + if (r < 0) + return r; + } + + if (i->with_monotonic && timestamp_is_set(t->monotonic)) { + _cleanup_free_ char *name = strjoin(METRIC_IO_SYSTEMD_MANAGER_PREFIX, i->name, ".Monotonic"); + if (!name) + return -ENOMEM; + + r = metric_build_send_unsigned( + &(const MetricFamily) { .name = name }, + vl, + /* object= */ NULL, + t->monotonic, + /* fields= */ NULL); + if (r < 0) + return r; + } } return 0; } -static int kernel_timestamp_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { - Manager *manager = ASSERT_PTR(userdata); - return boot_timestamp_build_json(mf, vl, &manager->timestamps[MANAGER_TIMESTAMP_KERNEL], /* with_monotonic= */ false); -} +static int manager_timestamps_describe(sd_varlink *link) { + int r; -static int userspace_timestamp_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { - Manager *manager = ASSERT_PTR(userdata); - return boot_timestamp_build_json(mf, vl, &manager->timestamps[MANAGER_TIMESTAMP_USERSPACE], /* with_monotonic= */ true); -} + assert(link); -static int finish_timestamp_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { - Manager *manager = ASSERT_PTR(userdata); - return boot_timestamp_build_json(mf, vl, &manager->timestamps[MANAGER_TIMESTAMP_FINISH], /* with_monotonic= */ true); + FOREACH_ELEMENT(i, manager_timestamp_metrics) { + if (!i->name) + continue; + + _cleanup_free_ char *rt_name = strjoin(METRIC_IO_SYSTEMD_MANAGER_PREFIX, i->name, ".Realtime"); + _cleanup_free_ char *rt_description = strjoin("CLOCK_REALTIME ", i->description); + if (!rt_name || !rt_description) + return -ENOMEM; + + r = metric_family_describe( + &(const MetricFamily) { + .name = rt_name, + .description = rt_description, + .type = METRIC_FAMILY_TYPE_GAUGE, + }, + link); + if (r < 0) + return r; + + if (i->with_monotonic) { + _cleanup_free_ char *mt_name = strjoin(METRIC_IO_SYSTEMD_MANAGER_PREFIX, i->name, ".Monotonic"); + _cleanup_free_ char *mt_description = strjoin("CLOCK_MONOTONIC ", i->description); + if (!mt_name || !mt_description) + return -ENOMEM; + + r = metric_family_describe( + &(const MetricFamily) { + .name = mt_name, + .description = mt_description, + .type = METRIC_FAMILY_TYPE_GAUGE, + }, + link); + if (r < 0) + return r; + } + } + + return 0; } static int state_change_timestamp_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { @@ -457,19 +631,6 @@ static const MetricFamily metric_family_table[] = { .type = METRIC_FAMILY_TYPE_GAUGE, .generate = active_timestamp_build_json, }, - { - .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "FinishTimestamp.Realtime", - .description = "CLOCK_REALTIME microseconds at which userspace finished booting", - .type = METRIC_FAMILY_TYPE_GAUGE, - .generate = finish_timestamp_build_json, - }, - { - .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "FinishTimestamp.Monotonic", - .description = "CLOCK_MONOTONIC microseconds at which userspace finished booting", - .type = METRIC_FAMILY_TYPE_GAUGE, - .generate = NULL, - }, - /* Keep those ↑ in sync with finish_timestamp_build_json(). */ { .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "InactiveExitTimestamp", .description = "Per unit metric: timestamp when the unit last exited the inactive state in microseconds; 0 indicates the transition has not occurred", @@ -482,12 +643,6 @@ static const MetricFamily metric_family_table[] = { .type = METRIC_FAMILY_TYPE_GAUGE, .generate = jobs_queued_build_json, }, - { - .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "KernelTimestamp.Realtime", - .description = "CLOCK_REALTIME microseconds at which the kernel started (CLOCK_MONOTONIC == 0)", - .type = METRIC_FAMILY_TYPE_GAUGE, - .generate = kernel_timestamp_build_json, - }, { .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "NRestarts", .description = "Per unit metric: number of restarts", @@ -554,19 +709,6 @@ static const MetricFamily metric_family_table[] = { .type = METRIC_FAMILY_TYPE_GAUGE, .generate = units_total_build_json, }, - { - .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "UserspaceTimestamp.Realtime", - .description = "CLOCK_REALTIME microseconds at which userspace was reached", - .type = METRIC_FAMILY_TYPE_GAUGE, - .generate = userspace_timestamp_build_json, - }, - { - .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "UserspaceTimestamp.Monotonic", - .description = "CLOCK_MONOTONIC microseconds at which userspace was reached", - .type = METRIC_FAMILY_TYPE_GAUGE, - .generate = NULL, - }, - /* Keep those ↑ in sync with userspace_timestamp_build_json(). */ { .name = METRIC_IO_SYSTEMD_MANAGER_PREFIX "Version", .description = "Version of systemd", @@ -577,9 +719,21 @@ static const MetricFamily metric_family_table[] = { }; int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { - return metrics_method_describe(metric_family_table, link, parameters, flags, userdata); + int r; + + r = metrics_method_describe(metric_family_table, link, parameters, flags, userdata); + if (r < 0) + return r; + + return manager_timestamps_describe(link); } int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { - return metrics_method_list(metric_family_table, link, parameters, flags, userdata); + int r; + + r = metrics_method_list(metric_family_table, link, parameters, flags, userdata); + if (r < 0) + return r; + + return manager_timestamps_build_json(link, userdata); } diff --git a/src/shared/metrics.c b/src/shared/metrics.c index cb00977b539c8..68b18895b4288 100644 --- a/src/shared/metrics.c +++ b/src/shared/metrics.c @@ -73,6 +73,20 @@ static int metric_family_build_json(const MetricFamily *mf, sd_json_variant **re SD_JSON_BUILD_PAIR_STRING("type", metric_family_type_to_string(mf->type))); } +int metric_family_describe(const MetricFamily *mf, sd_varlink *link) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + assert(mf); + assert(link); + + r = metric_family_build_json(mf, &v); + if (r < 0) + return r; + + return sd_varlink_reply(link, v); +} + int metrics_method_describe( const MetricFamily mfs[], sd_varlink *link, @@ -95,15 +109,9 @@ int metrics_method_describe( return r; for (const MetricFamily *mf = mfs; mf->name; mf++) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; - - r = metric_family_build_json(mf, &v); + r = metric_family_describe(mf, link); if (r < 0) return log_debug_errno(r, "Failed to describe metric family '%s': %m", mf->name); - - r = sd_varlink_reply(link, v); - if (r < 0) - return log_debug_errno(r, "Failed to send varlink reply: %m"); } return 0; diff --git a/src/shared/metrics.h b/src/shared/metrics.h index 8d6dcec999457..253e950fea7a6 100644 --- a/src/shared/metrics.h +++ b/src/shared/metrics.h @@ -34,6 +34,7 @@ int metrics_setup_varlink_server( DECLARE_STRING_TABLE_LOOKUP_TO_STRING(metric_family_type, MetricFamilyType); +int metric_family_describe(const MetricFamily *mf, sd_varlink *link); int metrics_method_describe(const MetricFamily mfs[], sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); int metrics_method_list(const MetricFamily mfs[], sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh index 06914fdc50324..627e7fef6554e 100755 --- a/test/units/TEST-74-AUX-UTILS.report.sh +++ b/test/units/TEST-74-AUX-UTILS.report.sh @@ -109,6 +109,17 @@ fi [ "$(metric_value UserspaceTimestamp.Realtime)" -gt 0 ] [ "$(metric_value UserspaceTimestamp.Monotonic)" -gt 0 ] +# These startup phases are captured unconditionally by manager_startup() on every boot (in containers +# too, where they are not redirected to the InitRD* variants), so both clocks are always set on the +# system manager by the time this service runs. We don't check UnitsLoadTimestamp (the last load, only +# set on the reload/reexec triggered by switch-root) nor the Security/Firmware/Loader/InitRD timestamps, +# which depend on the boot environment and may legitimately be unset (the metric is then suppressed). +for phase in GeneratorsStartTimestamp GeneratorsFinishTimestamp \ + UnitsLoadStartTimestamp UnitsLoadFinishTimestamp; do + [ "$(metric_value "$phase.Realtime")" -gt 0 ] + [ "$(metric_value "$phase.Monotonic")" -gt 0 ] +done + # test io.systemd.Basic.MachineInfo.* metrics, sourced from /etc/machine-info if [ -e /etc/machine-info ]; then MACHINE_INFO_BACKUP="$(mktemp)" From cb615e64ed7c2010b63382c22da97eae33805a68 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 16:56:20 +0100 Subject: [PATCH 02/71] report: drop redundant .generate = NULL metric initializers The .generate field of MetricFamily defaults to NULL when omitted from a designated initializer, so spelling it out explicitly on the metric-family entries (and the field-generating macros) that have no generator of their own is just noise. Drop it. --- src/report/report-basic.c | 6 ------ src/report/report-cgroup.c | 4 ---- 2 files changed, 10 deletions(-) diff --git a/src/report/report-basic.c b/src/report/report-basic.c index 56f7065c68d31..cc3cdd8b243a4 100644 --- a/src/report/report-basic.c +++ b/src/report/report-basic.c @@ -505,7 +505,6 @@ static int virtualization_generate(const MetricFamily *mf, sd_varlink *link, voi METRIC_IO_SYSTEMD_BASIC_PREFIX "OSRelease." name, \ "Operating system identification (" name "= field from os-release)", \ METRIC_FAMILY_TYPE_STRING, \ - .generate = NULL, \ } #define MACHINE_INFO_STANDARD_FIELD(name) \ @@ -513,7 +512,6 @@ static int virtualization_generate(const MetricFamily *mf, sd_varlink *link, voi METRIC_IO_SYSTEMD_BASIC_PREFIX "MachineInfo." name, \ "Machine identification (" name "= field from machine-info)", \ METRIC_FAMILY_TYPE_STRING, \ - .generate = NULL, \ } #define SMBIOS_STANDARD_FIELD(name) \ @@ -521,7 +519,6 @@ static int virtualization_generate(const MetricFamily *mf, sd_varlink *link, voi METRIC_IO_SYSTEMD_BASIC_PREFIX "SMBIOS." name, \ "Firmware/hardware identification (" name " field from SMBIOS/DMI)", \ METRIC_FAMILY_TYPE_STRING, \ - .generate = NULL, \ } static const MetricFamily metric_family_table[] = { @@ -572,13 +569,11 @@ static const MetricFamily metric_family_table[] = { METRIC_IO_SYSTEMD_BASIC_PREFIX "LoadAverage5Min", "System load average over the last 5 minutes", METRIC_FAMILY_TYPE_GAUGE, - .generate = NULL, }, { METRIC_IO_SYSTEMD_BASIC_PREFIX "LoadAverage15Min", "System load average over the last 15 minutes", METRIC_FAMILY_TYPE_GAUGE, - .generate = NULL, }, /* Keep those ↑ in sync with load_average_generate(). */ { @@ -665,7 +660,6 @@ static const MetricFamily metric_family_table[] = { METRIC_IO_SYSTEMD_BASIC_PREFIX "TPM2.VendorString", "TPM2 device vendor string (ID_TPM2_VENDOR_STRING property of the tpmrm0 device)", METRIC_FAMILY_TYPE_STRING, - .generate = NULL, }, /* Keep those ↑ in sync with tpm2_generate(). */ { diff --git a/src/report/report-cgroup.c b/src/report/report-cgroup.c index 240f09f9f2a4f..94aa0869c4540 100644 --- a/src/report/report-cgroup.c +++ b/src/report/report-cgroup.c @@ -370,25 +370,21 @@ static const MetricFamily cgroup_metric_family_table[] = { METRIC_IO_SYSTEMD_CGROUP_PREFIX "IOReadBytes", "Per unit metric: IO bytes read", METRIC_FAMILY_TYPE_COUNTER, - .generate = NULL, }, { METRIC_IO_SYSTEMD_CGROUP_PREFIX "IOReadOperations", "Per unit metric: IO read operations", METRIC_FAMILY_TYPE_COUNTER, - .generate = NULL, }, { METRIC_IO_SYSTEMD_CGROUP_PREFIX "MemoryUsage", "Per unit metric: memory usage in bytes", METRIC_FAMILY_TYPE_GAUGE, - .generate = NULL, }, { METRIC_IO_SYSTEMD_CGROUP_PREFIX "TasksCurrent", "Per unit metric: current number of tasks", METRIC_FAMILY_TYPE_GAUGE, - .generate = NULL, }, {} }; From 6268b094ebc288645bbaf0922070b31b52854cf5 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 16:06:28 +0900 Subject: [PATCH 03/71] resolve: fix segfault when built with OPENSSL_NO_DEPRECATED_3_0 In that case, deprecated funcdions are not loaded from libcrypto.so, and calling them causes segfault. --- src/resolve/resolved-dns-dnssec.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 36eb3595d5bec..6e5b4a0e0283e 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -79,9 +79,11 @@ static int dnssec_rsa_verify_raw( const void *data, size_t data_size, const void *exponent, size_t exponent_size, const void *modulus, size_t modulus_size) { - int r; +#if !defined(OPENSSL_NO_DEPRECATED_3_0) DISABLE_WARNING_DEPRECATED_DECLARATIONS; + int r; + _cleanup_(RSA_freep) RSA *rpubkey = NULL; _cleanup_(EVP_PKEY_freep) EVP_PKEY *epubkey = NULL; _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL; @@ -134,6 +136,9 @@ static int dnssec_rsa_verify_raw( REENABLE_WARNING; return r; +#else + return -EOPNOTSUPP; +#endif } static int dnssec_rsa_verify( @@ -204,9 +209,11 @@ static int dnssec_ecdsa_verify_raw( const void *signature_s, size_t signature_s_size, const void *data, size_t data_size, const void *key, size_t key_size) { - int k; +#if !defined(OPENSSL_NO_DEPRECATED_3_0) DISABLE_WARNING_DEPRECATED_DECLARATIONS; + int k; + _cleanup_(EC_GROUP_freep) EC_GROUP *ec_group = NULL; _cleanup_(EC_POINT_freep) EC_POINT *p = NULL; _cleanup_(EC_KEY_freep) EC_KEY *eckey = NULL; @@ -268,6 +275,9 @@ static int dnssec_ecdsa_verify_raw( REENABLE_WARNING; return k; +#else + return -EOPNOTSUPP; +#endif } static int dnssec_ecdsa_verify( From 5fec7096cfa27760d0a7de7fd5587c33549e8ca9 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 16:09:37 +0900 Subject: [PATCH 04/71] crypto-util: load several more symbols They will be used in later commits. Note, ECDSA_SIG_new() and ECDSA_SIG_set0() are not deprecated. They are moved to the section for non-deprecated symbols. --- src/shared/crypto-util.c | 36 ++++++++++++++++++++++++++++++------ src/shared/crypto-util.h | 18 ++++++++++++++++-- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index 57da78b00355e..281085688898c 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -100,6 +100,10 @@ DLSYM_PROTOTYPE(BN_set_word) = NULL; DLSYM_PROTOTYPE(BN_sub_word) = NULL; DLSYM_PROTOTYPE(CRYPTO_free) = NULL; DLSYM_PROTOTYPE(ECDSA_SIG_free) = NULL; +DLSYM_PROTOTYPE(ECDSA_SIG_get0_r) = NULL; +DLSYM_PROTOTYPE(ECDSA_SIG_get0_s) = NULL; +DLSYM_PROTOTYPE(ECDSA_SIG_new) = NULL; +DLSYM_PROTOTYPE(ECDSA_SIG_set0) = NULL; DLSYM_PROTOTYPE(EC_GROUP_free) = NULL; DLSYM_PROTOTYPE(EC_GROUP_get0_generator) = NULL; DLSYM_PROTOTYPE(EC_GROUP_get0_order) = NULL; @@ -170,7 +174,8 @@ DLSYM_PROTOTYPE(EVP_PKEY_CTX_new) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_CTX_new_from_name) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_CTX_new_id) = NULL; static DLSYM_PROTOTYPE(EVP_PKEY_CTX_set0_rsa_oaep_label) = NULL; -static DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_ec_paramgen_curve_nid) = NULL; +DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_ec_paramgen_curve_nid) = NULL; +DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_rsa_keygen_bits) = NULL; static DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_rsa_oaep_md) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_rsa_padding) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_signature_md) = NULL; @@ -183,17 +188,23 @@ DLSYM_PROTOTYPE(EVP_PKEY_eq) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_free) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_fromdata) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_fromdata_init) = NULL; +DLSYM_PROTOTYPE(EVP_PKEY_generate) = NULL; static DLSYM_PROTOTYPE(EVP_PKEY_get1_encoded_public_key) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_get_base_id) = NULL; static DLSYM_PROTOTYPE(EVP_PKEY_get_bits) = NULL; -static DLSYM_PROTOTYPE(EVP_PKEY_get_bn_param) = NULL; +DLSYM_PROTOTYPE(EVP_PKEY_get_bn_param) = NULL; static DLSYM_PROTOTYPE(EVP_PKEY_get_group_name) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_get_id) = NULL; +DLSYM_PROTOTYPE(EVP_PKEY_get_octet_string_param) = NULL; +DLSYM_PROTOTYPE(EVP_PKEY_get_size) = NULL; static DLSYM_PROTOTYPE(EVP_PKEY_get_utf8_string_param) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_keygen) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_keygen_init) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_new) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_new_raw_public_key) = NULL; +DLSYM_PROTOTYPE(EVP_PKEY_public_check) = NULL; +DLSYM_PROTOTYPE(EVP_PKEY_sign) = NULL; +DLSYM_PROTOTYPE(EVP_PKEY_sign_init) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_verify) = NULL; DLSYM_PROTOTYPE(EVP_PKEY_verify_init) = NULL; DLSYM_PROTOTYPE(EVP_aes_256_ctr) = NULL; @@ -217,6 +228,7 @@ DLSYM_PROTOTYPE(OPENSSL_sk_value) = NULL; DLSYM_PROTOTYPE(OSSL_EC_curve_nid2name) = NULL; DLSYM_PROTOTYPE(OSSL_PARAM_BLD_free) = NULL; DLSYM_PROTOTYPE(OSSL_PARAM_BLD_new) = NULL; +DLSYM_PROTOTYPE(OSSL_PARAM_BLD_push_BN) = NULL; static DLSYM_PROTOTYPE(OSSL_PARAM_BLD_push_octet_string) = NULL; DLSYM_PROTOTYPE(OSSL_PARAM_BLD_push_utf8_string) = NULL; DLSYM_PROTOTYPE(OSSL_PARAM_BLD_to_param) = NULL; @@ -284,11 +296,13 @@ static DLSYM_PROTOTYPE(X509_get_signature_info) = NULL; DLSYM_PROTOTYPE(X509_get_subject_name) = NULL; DLSYM_PROTOTYPE(X509_gmtime_adj) = NULL; DLSYM_PROTOTYPE(d2i_ASN1_OCTET_STRING) = NULL; +DLSYM_PROTOTYPE(d2i_ECDSA_SIG) = NULL; DLSYM_PROTOTYPE(d2i_ECPKParameters) = NULL; DLSYM_PROTOTYPE(d2i_PKCS7) = NULL; DLSYM_PROTOTYPE(d2i_PUBKEY) = NULL; DLSYM_PROTOTYPE(d2i_X509) = NULL; DLSYM_PROTOTYPE(i2d_ASN1_INTEGER) = NULL; +DLSYM_PROTOTYPE(i2d_ECDSA_SIG) = NULL; DLSYM_PROTOTYPE(i2d_PKCS7) = NULL; DLSYM_PROTOTYPE(i2d_PKCS7_fp) = NULL; DLSYM_PROTOTYPE(i2d_PUBKEY) = NULL; @@ -315,8 +329,6 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(ENGINE*, sym_ENGINE_free, ENGINE_freep, #if !defined(OPENSSL_NO_DEPRECATED_3_0) DISABLE_WARNING_DEPRECATED_DECLARATIONS; -DLSYM_PROTOTYPE(ECDSA_SIG_new) = NULL; -DLSYM_PROTOTYPE(ECDSA_SIG_set0) = NULL; DLSYM_PROTOTYPE(ECDSA_do_verify) = NULL; DLSYM_PROTOTYPE(EC_KEY_check_key) = NULL; DLSYM_PROTOTYPE(EC_KEY_free) = NULL; @@ -428,6 +440,10 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG(BN_sub_word), DLSYM_ARG(CRYPTO_free), DLSYM_ARG(ECDSA_SIG_free), + DLSYM_ARG(ECDSA_SIG_get0_r), + DLSYM_ARG(ECDSA_SIG_get0_s), + DLSYM_ARG(ECDSA_SIG_new), + DLSYM_ARG(ECDSA_SIG_set0), DLSYM_ARG(EC_GROUP_free), DLSYM_ARG(EC_GROUP_get0_generator), DLSYM_ARG(EC_GROUP_get0_order), @@ -499,6 +515,7 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG(EVP_PKEY_CTX_new_id), DLSYM_ARG(EVP_PKEY_CTX_set0_rsa_oaep_label), DLSYM_ARG(EVP_PKEY_CTX_set_ec_paramgen_curve_nid), + DLSYM_ARG(EVP_PKEY_CTX_set_rsa_keygen_bits), DLSYM_ARG(EVP_PKEY_CTX_set_rsa_oaep_md), DLSYM_ARG(EVP_PKEY_CTX_set_rsa_padding), DLSYM_ARG(EVP_PKEY_CTX_set_signature_md), @@ -511,17 +528,23 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG(EVP_PKEY_free), DLSYM_ARG(EVP_PKEY_fromdata), DLSYM_ARG(EVP_PKEY_fromdata_init), + DLSYM_ARG(EVP_PKEY_generate), DLSYM_ARG(EVP_PKEY_get1_encoded_public_key), DLSYM_ARG(EVP_PKEY_get_base_id), DLSYM_ARG(EVP_PKEY_get_bits), DLSYM_ARG(EVP_PKEY_get_bn_param), DLSYM_ARG(EVP_PKEY_get_group_name), DLSYM_ARG(EVP_PKEY_get_id), + DLSYM_ARG(EVP_PKEY_get_octet_string_param), + DLSYM_ARG(EVP_PKEY_get_size), DLSYM_ARG(EVP_PKEY_get_utf8_string_param), DLSYM_ARG(EVP_PKEY_keygen), DLSYM_ARG(EVP_PKEY_keygen_init), DLSYM_ARG(EVP_PKEY_new), DLSYM_ARG(EVP_PKEY_new_raw_public_key), + DLSYM_ARG(EVP_PKEY_public_check), + DLSYM_ARG(EVP_PKEY_sign), + DLSYM_ARG(EVP_PKEY_sign_init), DLSYM_ARG(EVP_PKEY_verify), DLSYM_ARG(EVP_PKEY_verify_init), DLSYM_ARG(EVP_aes_256_ctr), @@ -545,6 +568,7 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG(OSSL_EC_curve_nid2name), DLSYM_ARG(OSSL_PARAM_BLD_free), DLSYM_ARG(OSSL_PARAM_BLD_new), + DLSYM_ARG(OSSL_PARAM_BLD_push_BN), DLSYM_ARG(OSSL_PARAM_BLD_push_octet_string), DLSYM_ARG(OSSL_PARAM_BLD_push_utf8_string), DLSYM_ARG(OSSL_PARAM_BLD_to_param), @@ -612,11 +636,13 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG(X509_get_subject_name), DLSYM_ARG(X509_gmtime_adj), DLSYM_ARG(d2i_ASN1_OCTET_STRING), + DLSYM_ARG(d2i_ECDSA_SIG), DLSYM_ARG(d2i_ECPKParameters), DLSYM_ARG(d2i_PKCS7), DLSYM_ARG(d2i_PUBKEY), DLSYM_ARG(d2i_X509), DLSYM_ARG(i2d_ASN1_INTEGER), + DLSYM_ARG(i2d_ECDSA_SIG), DLSYM_ARG(i2d_PKCS7), DLSYM_ARG(i2d_PKCS7_fp), DLSYM_ARG(i2d_PUBKEY), @@ -631,8 +657,6 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG_FORCE(ENGINE_load_private_key), #endif #if !defined(OPENSSL_NO_DEPRECATED_3_0) - DLSYM_ARG_FORCE(ECDSA_SIG_new), - DLSYM_ARG_FORCE(ECDSA_SIG_set0), DLSYM_ARG_FORCE(ECDSA_do_verify), DLSYM_ARG_FORCE(EC_KEY_check_key), DLSYM_ARG_FORCE(EC_KEY_free), diff --git a/src/shared/crypto-util.h b/src/shared/crypto-util.h index 53da4cabe6f56..6ecf66bbf65df 100644 --- a/src/shared/crypto-util.h +++ b/src/shared/crypto-util.h @@ -122,6 +122,10 @@ extern DLSYM_PROTOTYPE(BN_set_word); extern DLSYM_PROTOTYPE(BN_sub_word); extern DLSYM_PROTOTYPE(CRYPTO_free); extern DLSYM_PROTOTYPE(ECDSA_SIG_free); +extern DLSYM_PROTOTYPE(ECDSA_SIG_get0_r); +extern DLSYM_PROTOTYPE(ECDSA_SIG_get0_s); +extern DLSYM_PROTOTYPE(ECDSA_SIG_new); +extern DLSYM_PROTOTYPE(ECDSA_SIG_set0); extern DLSYM_PROTOTYPE(EC_GROUP_free); extern DLSYM_PROTOTYPE(EC_GROUP_get0_generator); extern DLSYM_PROTOTYPE(EC_GROUP_get0_order); @@ -175,18 +179,27 @@ extern DLSYM_PROTOTYPE(EVP_PKEY_CTX_free); extern DLSYM_PROTOTYPE(EVP_PKEY_CTX_new); extern DLSYM_PROTOTYPE(EVP_PKEY_CTX_new_from_name); extern DLSYM_PROTOTYPE(EVP_PKEY_CTX_new_id); +extern DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_ec_paramgen_curve_nid); +extern DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_rsa_keygen_bits); extern DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_rsa_padding); extern DLSYM_PROTOTYPE(EVP_PKEY_CTX_set_signature_md); extern DLSYM_PROTOTYPE(EVP_PKEY_eq); extern DLSYM_PROTOTYPE(EVP_PKEY_free); extern DLSYM_PROTOTYPE(EVP_PKEY_fromdata); extern DLSYM_PROTOTYPE(EVP_PKEY_fromdata_init); +extern DLSYM_PROTOTYPE(EVP_PKEY_generate); extern DLSYM_PROTOTYPE(EVP_PKEY_get_base_id); +extern DLSYM_PROTOTYPE(EVP_PKEY_get_bn_param); extern DLSYM_PROTOTYPE(EVP_PKEY_get_id); +extern DLSYM_PROTOTYPE(EVP_PKEY_get_octet_string_param); +extern DLSYM_PROTOTYPE(EVP_PKEY_get_size); extern DLSYM_PROTOTYPE(EVP_PKEY_keygen); extern DLSYM_PROTOTYPE(EVP_PKEY_keygen_init); extern DLSYM_PROTOTYPE(EVP_PKEY_new); extern DLSYM_PROTOTYPE(EVP_PKEY_new_raw_public_key); +extern DLSYM_PROTOTYPE(EVP_PKEY_public_check); +extern DLSYM_PROTOTYPE(EVP_PKEY_sign); +extern DLSYM_PROTOTYPE(EVP_PKEY_sign_init); extern DLSYM_PROTOTYPE(EVP_PKEY_verify); extern DLSYM_PROTOTYPE(EVP_PKEY_verify_init); extern DLSYM_PROTOTYPE(EVP_aes_256_ctr); @@ -210,6 +223,7 @@ extern DLSYM_PROTOTYPE(OPENSSL_sk_value); extern DLSYM_PROTOTYPE(OSSL_EC_curve_nid2name); extern DLSYM_PROTOTYPE(OSSL_PARAM_BLD_free); extern DLSYM_PROTOTYPE(OSSL_PARAM_BLD_new); +extern DLSYM_PROTOTYPE(OSSL_PARAM_BLD_push_BN); extern DLSYM_PROTOTYPE(OSSL_PARAM_BLD_push_utf8_string); extern DLSYM_PROTOTYPE(OSSL_PARAM_BLD_to_param); extern DLSYM_PROTOTYPE(OSSL_PARAM_construct_BN); @@ -256,11 +270,13 @@ extern DLSYM_PROTOTYPE(X509_get_pubkey); extern DLSYM_PROTOTYPE(X509_get_subject_name); extern DLSYM_PROTOTYPE(X509_gmtime_adj); extern DLSYM_PROTOTYPE(d2i_ASN1_OCTET_STRING); +extern DLSYM_PROTOTYPE(d2i_ECDSA_SIG); extern DLSYM_PROTOTYPE(d2i_ECPKParameters); extern DLSYM_PROTOTYPE(d2i_PKCS7); extern DLSYM_PROTOTYPE(d2i_PUBKEY); extern DLSYM_PROTOTYPE(d2i_X509); extern DLSYM_PROTOTYPE(i2d_ASN1_INTEGER); +extern DLSYM_PROTOTYPE(i2d_ECDSA_SIG); extern DLSYM_PROTOTYPE(i2d_PKCS7); extern DLSYM_PROTOTYPE(i2d_PKCS7_fp); extern DLSYM_PROTOTYPE(i2d_PUBKEY); @@ -269,8 +285,6 @@ extern DLSYM_PROTOTYPE(i2d_X509_NAME); #if !defined(OPENSSL_NO_DEPRECATED_3_0) DISABLE_WARNING_DEPRECATED_DECLARATIONS; -extern DLSYM_PROTOTYPE(ECDSA_SIG_new); -extern DLSYM_PROTOTYPE(ECDSA_SIG_set0); extern DLSYM_PROTOTYPE(ECDSA_do_verify); extern DLSYM_PROTOTYPE(EC_KEY_check_key); extern DLSYM_PROTOTYPE(EC_KEY_free); From 6bf0540e936b469109dde304f05fd3bfb5e28c3c Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 04:34:56 +0900 Subject: [PATCH 05/71] resolve: add unit test for dnssec_rsa_verify_raw() --- src/resolve/meson.build | 4 + src/resolve/resolved-dns-dnssec-crypto.h | 16 ++ src/resolve/resolved-dns-dnssec.c | 4 +- src/resolve/test-dnssec-crypto.c | 260 +++++++++++++++++++++++ 4 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 src/resolve/resolved-dns-dnssec-crypto.h create mode 100644 src/resolve/test-dnssec-crypto.c diff --git a/src/resolve/meson.build b/src/resolve/meson.build index f024b02047c7b..019c4b7bb06c4 100644 --- a/src/resolve/meson.build +++ b/src/resolve/meson.build @@ -131,6 +131,10 @@ executables += [ 'sources' : files('test-dnssec-complex.c'), 'type' : 'manual', }, + resolve_test_template + { + 'sources' : files('test-dnssec-crypto.c'), + 'conditions' : ['HAVE_OPENSSL'], + }, resolve_test_template + { 'sources' : files('test-dns-search-domain.c'), }, diff --git a/src/resolve/resolved-dns-dnssec-crypto.h b/src/resolve/resolved-dns-dnssec-crypto.h new file mode 100644 index 0000000000000..018bab7ba5a4b --- /dev/null +++ b/src/resolve/resolved-dns-dnssec-crypto.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "crypto-util.h" +#include "shared-forward.h" + +#if HAVE_OPENSSL + +int dnssec_rsa_verify_raw( + const EVP_MD *hash_algorithm, + const void *signature, size_t signature_size, + const void *data, size_t data_size, + const void *exponent, size_t exponent_size, + const void *modulus, size_t modulus_size); + +#endif diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 6e5b4a0e0283e..3e370d8d7168b 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -12,12 +12,12 @@ #include "memory-util.h" #include "memstream-util.h" #include "resolved-dns-dnssec.h" +#include "resolved-dns-dnssec-crypto.h" #include "sort-util.h" #include "string-table.h" #include "string-util.h" #include "time-util.h" - #define VERIFY_RRS_MAX 256 #define MAX_KEY_SIZE (32*1024) @@ -73,7 +73,7 @@ static int dnssec_verify_errno(int r) { return r == -EOPNOTSUPP ? -EIO : r; } -static int dnssec_rsa_verify_raw( +int dnssec_rsa_verify_raw( const EVP_MD *hash_algorithm, const void *signature, size_t signature_size, const void *data, size_t data_size, diff --git a/src/resolve/test-dnssec-crypto.c b/src/resolve/test-dnssec-crypto.c new file mode 100644 index 0000000000000..66744795e0f5d --- /dev/null +++ b/src/resolve/test-dnssec-crypto.c @@ -0,0 +1,260 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#if HAVE_VALGRIND_VALGRIND_H +# include +#endif + +#include "hexdecoct.h" +#include "resolved-dns-dnssec-crypto.h" +#include "tests.h" + +static void iovec_dump_c(const char *name, const struct iovec *iov) { + assert(iovec_is_set(iov)); + + printf("static const uint8_t %s[] = {", name); + const uint8_t *buf = iov->iov_base; + for (size_t i = 0; i < iov->iov_len; i++) { + if (i % 8 == 0) + fputs("\n ", stdout); + else + putchar(' '); + + fputs("0x", stdout); + putchar(hexchar((buf[i] & 0xf0) >> 4)); + putchar(hexchar((buf[i] & 0x0f) >> 0)); + putchar(','); + } + puts("\n};\n"); +} + +static void export_bn(const BIGNUM *x, struct iovec *ret) { + assert(x); + assert(ret); + + _cleanup_(iovec_done) struct iovec iov = {}; + ASSERT_OK(iovec_alloc(sym_BN_num_bytes(x), &iov)); + ASSERT_EQ(sym_BN_bn2bin(x, iov.iov_base), (int) iov.iov_len); + + *ret = TAKE_STRUCT(iov); +} + +static const uint8_t test_signature_buf[] = { + 0xa6, 0x6c, 0x60, 0xec, 0x89, 0x90, 0x74, 0xba, + 0x9a, 0x6b, 0x7b, 0xbe, 0xd5, 0x46, 0xfb, 0x5c, + 0xa1, 0x80, 0x15, 0x4c, 0x98, 0x1e, 0xae, 0x13, + 0x41, 0xf1, 0xd3, 0x9e, 0xe6, 0x4f, 0x57, 0x47, + 0x4b, 0xef, 0x3e, 0xe3, 0xdf, 0xef, 0x5e, 0x2f, + 0x76, 0xb2, 0x4a, 0x65, 0x72, 0x81, 0x19, 0xd7, + 0x6e, 0x1c, 0xe7, 0xed, 0x4b, 0x36, 0xd1, 0x06, + 0xaa, 0x86, 0x05, 0xbe, 0x9c, 0x8b, 0x69, 0x80, + 0x30, 0x7f, 0x13, 0xb0, 0x6f, 0xc8, 0x53, 0x42, + 0x00, 0xe2, 0x9e, 0xc6, 0x64, 0xcf, 0x83, 0xac, + 0x83, 0x38, 0x9a, 0xe7, 0xdf, 0x4a, 0x80, 0x08, + 0x22, 0x74, 0x6c, 0x14, 0x20, 0xfe, 0x7d, 0x92, + 0x5f, 0x53, 0xa8, 0xb6, 0x59, 0xd5, 0xae, 0x08, + 0x11, 0x30, 0x0f, 0xf2, 0x5d, 0x3d, 0x69, 0xa6, + 0x94, 0xf5, 0xfa, 0x58, 0xe3, 0x70, 0x75, 0x5b, + 0x68, 0xb1, 0x47, 0x10, 0x72, 0xbf, 0x97, 0xc5, + 0x96, 0x97, 0x05, 0x9a, 0xd5, 0x36, 0xa5, 0x84, + 0x99, 0x4d, 0x0d, 0xe9, 0x47, 0xeb, 0xa8, 0xac, + 0x63, 0xad, 0x70, 0x0d, 0xc1, 0x45, 0xf2, 0x84, + 0x5f, 0x58, 0xcc, 0xcc, 0x77, 0xb4, 0x46, 0xc2, + 0x7e, 0x30, 0xb6, 0x35, 0x76, 0x8c, 0xe8, 0x27, + 0xa3, 0x86, 0x32, 0x29, 0xe7, 0x72, 0x5e, 0x41, + 0x1f, 0x5d, 0x81, 0x35, 0xe8, 0x46, 0x17, 0x55, + 0x51, 0xc8, 0x88, 0x9e, 0x72, 0x9a, 0x78, 0xc2, + 0xed, 0x3c, 0x32, 0x54, 0xd6, 0x46, 0x94, 0xe1, + 0x9c, 0xce, 0xc1, 0x49, 0x2c, 0x43, 0x53, 0x7b, + 0x7d, 0xe3, 0xc5, 0x7e, 0x76, 0x5b, 0x4b, 0xe0, + 0xac, 0x3a, 0xaf, 0xea, 0xfd, 0xc6, 0x62, 0x64, + 0x0c, 0x17, 0xb7, 0x37, 0x6c, 0x1a, 0x7d, 0x69, + 0xea, 0x84, 0x54, 0x1b, 0xd5, 0x0e, 0x9e, 0x76, + 0x0a, 0x12, 0xb7, 0x29, 0xb4, 0xd2, 0x61, 0xc4, + 0x38, 0xb6, 0xfc, 0x34, 0xd2, 0x20, 0xb0, 0x90, +}; + +static const uint8_t test_digest_buf[] = { + 0x64, 0xec, 0x88, 0xca, 0x00, 0xb2, 0x68, 0xe5, + 0xba, 0x1a, 0x35, 0x67, 0x8a, 0x1b, 0x53, 0x16, + 0xd2, 0x12, 0xf4, 0xf3, 0x66, 0xb2, 0x47, 0x72, + 0x32, 0x53, 0x4a, 0x8a, 0xec, 0xa3, 0x7f, 0x3c, +}; + +static const uint8_t test_exponent_buf[] = { + 0x01, 0x00, 0x01, +}; + +static const uint8_t test_modulus_buf[] = { + 0xa6, 0xf9, 0xcc, 0xed, 0x81, 0x49, 0xf0, 0x83, + 0xa7, 0xe5, 0x15, 0x93, 0xdd, 0x64, 0xa1, 0x66, + 0x40, 0xf9, 0x46, 0xd2, 0x3d, 0x16, 0xfb, 0x84, + 0x50, 0x53, 0x55, 0xba, 0x87, 0xcb, 0x15, 0xb8, + 0x98, 0xa4, 0xd2, 0xb4, 0xa6, 0xb4, 0x41, 0x2b, + 0xb4, 0x32, 0x0a, 0xc7, 0x8a, 0xe0, 0xa0, 0x2f, + 0x4e, 0xf7, 0x57, 0x44, 0xd3, 0x27, 0x94, 0x8a, + 0x10, 0x71, 0xc5, 0x3d, 0xa7, 0x25, 0xc2, 0x3f, + 0xdd, 0x1a, 0xa3, 0x05, 0x73, 0x41, 0xd8, 0x1c, + 0x99, 0xfd, 0x7f, 0x84, 0xe5, 0x09, 0xd2, 0x89, + 0x93, 0x61, 0xc6, 0xd6, 0xac, 0x6e, 0x9d, 0xe0, + 0xfb, 0x86, 0x81, 0x4a, 0x2f, 0x0a, 0xe8, 0x11, + 0xc8, 0x7f, 0x2b, 0x8b, 0x1b, 0xa3, 0x00, 0x15, + 0x25, 0xf0, 0x44, 0x39, 0xcd, 0x43, 0xbc, 0x19, + 0xad, 0xfb, 0xd3, 0xa7, 0x3f, 0x63, 0xc1, 0x78, + 0x0b, 0x69, 0x46, 0xc8, 0xe5, 0x6e, 0x15, 0x75, + 0x08, 0x8e, 0xc5, 0x67, 0xb7, 0x51, 0x61, 0x8a, + 0xb1, 0xff, 0x0a, 0xc5, 0x11, 0x30, 0x71, 0xca, + 0x88, 0x02, 0x3b, 0xfc, 0x40, 0x06, 0x11, 0x8a, + 0x0c, 0x0f, 0xb2, 0x7c, 0xf4, 0xd7, 0x07, 0xf4, + 0x85, 0xcd, 0xb7, 0x70, 0xcc, 0x5e, 0x21, 0xa0, + 0xcf, 0xb3, 0xe7, 0x47, 0x3e, 0xab, 0x20, 0x72, + 0xc3, 0xea, 0xcd, 0xfd, 0xb8, 0x50, 0x7c, 0xaa, + 0x2f, 0xd3, 0xf2, 0xc1, 0x99, 0xbc, 0x9c, 0x34, + 0xfd, 0x53, 0x26, 0x87, 0x35, 0x37, 0x78, 0xac, + 0x19, 0x13, 0x43, 0xb3, 0x80, 0x92, 0x7d, 0xeb, + 0xe2, 0x05, 0x62, 0xbf, 0x50, 0xde, 0x18, 0x17, + 0x74, 0xff, 0xa2, 0x5b, 0xfd, 0x14, 0xbf, 0xdd, + 0x21, 0xa6, 0xcb, 0xb9, 0x67, 0xce, 0x50, 0x06, + 0x91, 0x26, 0x71, 0x97, 0x5c, 0xf1, 0x81, 0x3b, + 0x68, 0xe8, 0xd9, 0xdf, 0xd3, 0xe3, 0xcc, 0x61, + 0xae, 0x73, 0x6c, 0xc8, 0x6c, 0x7b, 0x67, 0xd3, +}; + +static const struct iovec test_signature = IOVEC_MAKE(test_signature_buf, sizeof(test_signature_buf)); +static const struct iovec test_digest = IOVEC_MAKE(test_digest_buf, sizeof(test_digest_buf)); +static const struct iovec test_exponent = IOVEC_MAKE(test_exponent_buf, sizeof(test_exponent_buf)); +static const struct iovec test_modulus = IOVEC_MAKE(test_modulus_buf, sizeof(test_modulus_buf)); + +TEST(generate_rsa_test_vectors) { + /* This does not test anything but generates test vectors for dnssec_rsa_verify_raw(). + * This is skipped when we are running on valgrind or sanitizers, as it is extremely slow. */ +#if HAVE_VALGRIND_VALGRIND_H + if (RUNNING_ON_VALGRIND) + return (void) log_tests_skipped("Running on valgrind"); +#endif +#if HAS_FEATURE_ADDRESS_SANITIZER + return (void) log_tests_skipped("Running on sanitizers"); +#endif + + const struct iovec test_message = CONST_IOVEC_MAKE_STRING("Hello world"); + + /* Generate a 2048-bit RSA key pair. */ + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *kctx = + ASSERT_NOT_NULL(sym_EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_keygen_init(kctx)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_CTX_set_rsa_keygen_bits(kctx, 2048)); + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; + ASSERT_OK_POSITIVE(sym_EVP_PKEY_generate(kctx, &pkey)); + + /* Export public key parameters. */ + _cleanup_(BN_freep) BIGNUM *n = NULL, *e = NULL; + ASSERT_OK_POSITIVE(sym_EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_N, &n)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_E, &e)); + _cleanup_(iovec_done) struct iovec modulus = {}, exponent = {}; + export_bn(n, &modulus); + export_bn(e, &exponent); + + /* Calculate SHA-256 digest. */ + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mctx = ASSERT_NOT_NULL(sym_EVP_MD_CTX_new()); + ASSERT_OK_POSITIVE(sym_EVP_DigestInit_ex(mctx, sym_EVP_sha256(), NULL)); + ASSERT_OK_POSITIVE(sym_EVP_DigestUpdate(mctx, test_message.iov_base, test_message.iov_len)); + struct iovec digest = IOVEC_ALLOCA(EVP_MAX_MD_SIZE); + unsigned len; + ASSERT_OK_POSITIVE(sym_EVP_DigestFinal_ex(mctx, digest.iov_base, &len)); + digest.iov_len = len; + + /* Sign the digest with RSA PKCS#1 v1.5. */ + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *sctx = ASSERT_NOT_NULL(sym_EVP_PKEY_CTX_new(pkey, NULL)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_sign_init(sctx)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_CTX_set_rsa_padding(sctx, RSA_PKCS1_PADDING)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_CTX_set_signature_md(sctx, sym_EVP_sha256())); + size_t sz; + ASSERT_OK_POSITIVE(sym_EVP_PKEY_sign(sctx, NULL, &sz, digest.iov_base, digest.iov_len)); + struct iovec signature = IOVEC_ALLOCA(sz); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_sign(sctx, signature.iov_base, &signature.iov_len, digest.iov_base, digest.iov_len)); + + iovec_dump_c("test_signature_buf", &signature); + iovec_dump_c("test_digest_buf", &digest); + iovec_dump_c("test_exponent_buf", &exponent); + iovec_dump_c("test_modulus_buf", &modulus); +} + +#define TEST_RSA_VERIFY(signature, digest, exponent, modulus, expected) \ + if (expected >= 0) \ + ASSERT_OK_EQ(dnssec_rsa_verify_raw( \ + sym_EVP_sha256(), \ + signature.iov_base, signature.iov_len, \ + digest.iov_base, digest.iov_len, \ + exponent.iov_base, exponent.iov_len, \ + modulus.iov_base, modulus.iov_len), \ + expected); \ + else \ + ASSERT_ERROR(dnssec_rsa_verify_raw( \ + sym_EVP_sha256(), \ + signature.iov_base, signature.iov_len, \ + digest.iov_base, digest.iov_len, \ + exponent.iov_base, exponent.iov_len, \ + modulus.iov_base, modulus.iov_len), \ + -expected); + +TEST(dnssec_rsa_verify_raw) { +#if !defined(OPENSSL_NO_DEPRECATED_3_0) + uint8_t *p; + + TEST_RSA_VERIFY(test_signature, test_digest, test_exponent, test_modulus, 1); + + _cleanup_(iovec_done) struct iovec bad_signature = {}; + ASSERT_NOT_NULL(iovec_memdup(&test_signature, &bad_signature)); + p = bad_signature.iov_base; + p[0] ^= 0x01; + TEST_RSA_VERIFY(bad_signature, test_digest, test_exponent, test_modulus, 0); + + p[0] ^= 0x01; + bad_signature.iov_len -= 1; + TEST_RSA_VERIFY(bad_signature, test_digest, test_exponent, test_modulus, -EINVAL); + + _cleanup_(iovec_done) struct iovec bad_digest = {}; + ASSERT_NOT_NULL(iovec_memdup(&test_digest, &bad_digest)); + p = bad_digest.iov_base; + p[0] ^= 0x01; + TEST_RSA_VERIFY(test_signature, bad_digest, test_exponent, test_modulus, 0); + + p[0] ^= 0x01; + bad_digest.iov_len -= 1; + TEST_RSA_VERIFY(test_signature, bad_digest, test_exponent, test_modulus, 0); + + _cleanup_(iovec_done) struct iovec bad_exponent = {}; + ASSERT_NOT_NULL(iovec_memdup(&test_exponent, &bad_exponent)); + p = bad_exponent.iov_base; + p[0] ^= 0x01; + TEST_RSA_VERIFY(test_signature, test_digest, bad_exponent, test_modulus, 0); + + p[0] ^= 0x01; + bad_exponent.iov_len -= 1; + TEST_RSA_VERIFY(test_signature, test_digest, bad_exponent, test_modulus, 0); + + _cleanup_(iovec_done) struct iovec bad_modulus = {}; + ASSERT_NOT_NULL(iovec_memdup(&test_modulus, &bad_modulus)); + p = bad_modulus.iov_base; + p[0] ^= 0x01; + TEST_RSA_VERIFY(test_signature, test_digest, test_exponent, bad_modulus, 0); + + p[0] ^= 0x01; + p[bad_modulus.iov_len - 1] ^= 0x01; + TEST_RSA_VERIFY(test_signature, test_digest, test_exponent, bad_modulus, 0); + + p[bad_modulus.iov_len - 1] ^= 0x01; + bad_modulus.iov_len -= 1; + TEST_RSA_VERIFY(test_signature, test_digest, test_exponent, bad_modulus, -EINVAL); +#else + TEST_RSA_VERIFY(test_signature, test_digest, test_exponent, test_modulus, -EOPNOTSUPP); +#endif +} + +static int intro(void) { + if (DLOPEN_LIBCRYPTO(LOG_DEBUG, SD_ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED) < 0) + return EXIT_TEST_SKIP; + + return EXIT_SUCCESS; +} + +DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro); From 918193b438111f9b26bfec974a62dd49ba113bb9 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 16:21:34 +0900 Subject: [PATCH 06/71] resolve: make dnssec_rsa_verify_raw() take struct iovec This also - adds missing assertions, - moves variable declarations. No functional change, just refactoring. --- src/resolve/resolved-dns-dnssec-crypto.h | 8 ++--- src/resolve/resolved-dns-dnssec.c | 46 ++++++++++++------------ src/resolve/test-dnssec-crypto.c | 16 ++++----- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/src/resolve/resolved-dns-dnssec-crypto.h b/src/resolve/resolved-dns-dnssec-crypto.h index 018bab7ba5a4b..c0b00e6ae2a16 100644 --- a/src/resolve/resolved-dns-dnssec-crypto.h +++ b/src/resolve/resolved-dns-dnssec-crypto.h @@ -8,9 +8,9 @@ int dnssec_rsa_verify_raw( const EVP_MD *hash_algorithm, - const void *signature, size_t signature_size, - const void *data, size_t data_size, - const void *exponent, size_t exponent_size, - const void *modulus, size_t modulus_size); + const struct iovec *signature, + const struct iovec *hash, + const struct iovec *exponent, + const struct iovec *modulus); #endif diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 3e370d8d7168b..7d07105250825 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -75,31 +75,30 @@ static int dnssec_verify_errno(int r) { int dnssec_rsa_verify_raw( const EVP_MD *hash_algorithm, - const void *signature, size_t signature_size, - const void *data, size_t data_size, - const void *exponent, size_t exponent_size, - const void *modulus, size_t modulus_size) { + const struct iovec *signature, + const struct iovec *hash, + const struct iovec *exponent, + const struct iovec *modulus) { #if !defined(OPENSSL_NO_DEPRECATED_3_0) DISABLE_WARNING_DEPRECATED_DECLARATIONS; int r; - _cleanup_(RSA_freep) RSA *rpubkey = NULL; - _cleanup_(EVP_PKEY_freep) EVP_PKEY *epubkey = NULL; - _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL; - _cleanup_(BN_freep) BIGNUM *e = NULL, *m = NULL; - assert(hash_algorithm); + assert(iovec_is_set(signature)); + assert(iovec_is_set(hash)); + assert(iovec_is_set(exponent)); + assert(iovec_is_set(modulus)); - e = sym_BN_bin2bn(exponent, exponent_size, NULL); + _cleanup_(BN_freep) BIGNUM *e = sym_BN_bin2bn(exponent->iov_base, exponent->iov_len, NULL); if (!e) return log_openssl_errors(LOG_DEBUG, "Failed to convert RSA exponent to BIGNUM"); - m = sym_BN_bin2bn(modulus, modulus_size, NULL); + _cleanup_(BN_freep) BIGNUM *m = sym_BN_bin2bn(modulus->iov_base, modulus->iov_len, NULL); if (!m) return log_openssl_errors(LOG_DEBUG, "Failed to convert RSA modulus to BIGNUM"); - rpubkey = sym_RSA_new(); + _cleanup_(RSA_freep) RSA *rpubkey = sym_RSA_new(); if (!rpubkey) return -ENOMEM; @@ -107,17 +106,17 @@ int dnssec_rsa_verify_raw( return log_openssl_errors(LOG_DEBUG, "Failed to set RSA public key"); e = m = NULL; - if ((size_t) sym_RSA_size(rpubkey) != signature_size) + if ((size_t) sym_RSA_size(rpubkey) != signature->iov_len) return -EINVAL; - epubkey = sym_EVP_PKEY_new(); + _cleanup_(EVP_PKEY_freep) EVP_PKEY *epubkey = sym_EVP_PKEY_new(); if (!epubkey) return -ENOMEM; if (sym_EVP_PKEY_assign_RSA(epubkey, sym_RSAPublicKey_dup(rpubkey)) <= 0) return log_openssl_errors(LOG_DEBUG, "Failed to assign RSA public key"); - ctx = sym_EVP_PKEY_CTX_new(epubkey, NULL); + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = sym_EVP_PKEY_CTX_new(epubkey, NULL); if (!ctx) return -ENOMEM; @@ -130,7 +129,7 @@ int dnssec_rsa_verify_raw( if (sym_EVP_PKEY_CTX_set_signature_md(ctx, hash_algorithm) <= 0) return log_openssl_errors(LOG_DEBUG, "Failed to set RSA signature digest"); - r = sym_EVP_PKEY_verify(ctx, signature, signature_size, data, data_size); + r = sym_EVP_PKEY_verify(ctx, signature->iov_base, signature->iov_len, hash->iov_base, hash->iov_len); if (r < 0) return log_openssl_errors(LOG_DEBUG, "Signature verification failed"); @@ -143,7 +142,7 @@ int dnssec_rsa_verify_raw( static int dnssec_rsa_verify( const EVP_MD *hash_algorithm, - const void *hash, size_t hash_size, + const struct iovec *hash, DnsResourceRecord *rrsig, DnsResourceRecord *dnskey) { @@ -151,8 +150,7 @@ static int dnssec_rsa_verify( void *exponent, *modulus; assert(hash_algorithm); - assert(hash); - assert(hash_size > 0); + assert(iovec_is_set(hash)); assert(rrsig); assert(dnskey); @@ -196,10 +194,10 @@ static int dnssec_rsa_verify( return dnssec_rsa_verify_raw( hash_algorithm, - rrsig->rrsig.signature, rrsig->rrsig.signature_size, - hash, hash_size, - exponent, exponent_size, - modulus, modulus_size); + &IOVEC_MAKE(rrsig->rrsig.signature, rrsig->rrsig.signature_size), + hash, + &IOVEC_MAKE(exponent, exponent_size), + &IOVEC_MAKE(modulus, modulus_size)); } static int dnssec_ecdsa_verify_raw( @@ -687,7 +685,7 @@ static int dnssec_rrset_verify_sig( case DNSSEC_ALGORITHM_RSASHA512: return dnssec_verify_errno(dnssec_rsa_verify( md_algorithm, - hash, hash_size, + &IOVEC_MAKE(hash, hash_size), rrsig, dnskey)); diff --git a/src/resolve/test-dnssec-crypto.c b/src/resolve/test-dnssec-crypto.c index 66744795e0f5d..c8b91bb1bacd2 100644 --- a/src/resolve/test-dnssec-crypto.c +++ b/src/resolve/test-dnssec-crypto.c @@ -182,18 +182,18 @@ TEST(generate_rsa_test_vectors) { if (expected >= 0) \ ASSERT_OK_EQ(dnssec_rsa_verify_raw( \ sym_EVP_sha256(), \ - signature.iov_base, signature.iov_len, \ - digest.iov_base, digest.iov_len, \ - exponent.iov_base, exponent.iov_len, \ - modulus.iov_base, modulus.iov_len), \ + &signature, \ + &digest, \ + &exponent, \ + &modulus), \ expected); \ else \ ASSERT_ERROR(dnssec_rsa_verify_raw( \ sym_EVP_sha256(), \ - signature.iov_base, signature.iov_len, \ - digest.iov_base, digest.iov_len, \ - exponent.iov_base, exponent.iov_len, \ - modulus.iov_base, modulus.iov_len), \ + &signature, \ + &digest, \ + &exponent, \ + &modulus), \ -expected); TEST(dnssec_rsa_verify_raw) { From cd3c0859b23f5b64e7f42d7bd68ae4195daf9784 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 17:00:27 +0900 Subject: [PATCH 07/71] resolved: migrate RSA key construction to OpenSSL 3 EVP API OpenSSL 3.0 deprecated low-level key manipulation functions and direct access to RSA structures (such as RSA_new(), RSA_set0_key(), and RSA_size()). This commit modernizes dnssec_rsa_verify_raw() by replacing these deprecated functions with the provider-aware EVP API: * Uses OSSL_PARAM_BLD and EVP_PKEY_fromdata() to construct the RSA public key directly from the modulus and exponent BIGNUMs. * Replaces RSA_size() with EVP_PKEY_get_size(). Consequently, the workaround macros suppressing deprecated warnings (DISABLE_WARNING_DEPRECATED_DECLARATIONS) and the conditional fallback blocks (#if !defined(OPENSSL_NO_DEPRECATED_3_0)) are no longer needed and have been dropped. Unit tests are also updated to run unconditionally. --- src/resolve/resolved-dns-dnssec.c | 38 +++++++++++++++++-------------- src/resolve/test-dnssec-crypto.c | 4 ---- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 7d07105250825..b9afdac9ffd4f 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -80,8 +80,6 @@ int dnssec_rsa_verify_raw( const struct iovec *exponent, const struct iovec *modulus) { -#if !defined(OPENSSL_NO_DEPRECATED_3_0) - DISABLE_WARNING_DEPRECATED_DECLARATIONS; int r; assert(hash_algorithm); @@ -98,23 +96,33 @@ int dnssec_rsa_verify_raw( if (!m) return log_openssl_errors(LOG_DEBUG, "Failed to convert RSA modulus to BIGNUM"); - _cleanup_(RSA_freep) RSA *rpubkey = sym_RSA_new(); - if (!rpubkey) + _cleanup_(OSSL_PARAM_BLD_freep) OSSL_PARAM_BLD *bld = sym_OSSL_PARAM_BLD_new(); + if (!bld) return -ENOMEM; - if (sym_RSA_set0_key(rpubkey, m, e, NULL) <= 0) - return log_openssl_errors(LOG_DEBUG, "Failed to set RSA public key"); - e = m = NULL; + if (sym_OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_E, e) <= 0) + return log_openssl_errors(LOG_DEBUG, "Failed to push RSA exponent to OSSL_PARAM_BLD"); - if ((size_t) sym_RSA_size(rpubkey) != signature->iov_len) - return -EINVAL; + if (sym_OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_N, m) <= 0) + return log_openssl_errors(LOG_DEBUG, "Failed to push RSA modulus to OSSL_PARAM_BLD"); - _cleanup_(EVP_PKEY_freep) EVP_PKEY *epubkey = sym_EVP_PKEY_new(); - if (!epubkey) + _cleanup_(OSSL_PARAM_freep) OSSL_PARAM *params = sym_OSSL_PARAM_BLD_to_param(bld); + if (!params) + return log_openssl_errors(LOG_DEBUG, "Failed to generate OSSL param"); + + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *kctx = sym_EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (!kctx) return -ENOMEM; - if (sym_EVP_PKEY_assign_RSA(epubkey, sym_RSAPublicKey_dup(rpubkey)) <= 0) - return log_openssl_errors(LOG_DEBUG, "Failed to assign RSA public key"); + if (sym_EVP_PKEY_fromdata_init(kctx) <= 0) + return log_openssl_errors(LOG_DEBUG, "Failed to initialize key creation"); + + _cleanup_(EVP_PKEY_freep) EVP_PKEY *epubkey = NULL; + if (sym_EVP_PKEY_fromdata(kctx, &epubkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) + return log_openssl_errors(LOG_DEBUG, "Failed to load RSA public key from raw data"); + + if ((size_t) sym_EVP_PKEY_get_size(epubkey) != signature->iov_len) + return -EINVAL; _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = sym_EVP_PKEY_CTX_new(epubkey, NULL); if (!ctx) @@ -133,11 +141,7 @@ int dnssec_rsa_verify_raw( if (r < 0) return log_openssl_errors(LOG_DEBUG, "Signature verification failed"); - REENABLE_WARNING; return r; -#else - return -EOPNOTSUPP; -#endif } static int dnssec_rsa_verify( diff --git a/src/resolve/test-dnssec-crypto.c b/src/resolve/test-dnssec-crypto.c index c8b91bb1bacd2..034320b348d8b 100644 --- a/src/resolve/test-dnssec-crypto.c +++ b/src/resolve/test-dnssec-crypto.c @@ -197,7 +197,6 @@ TEST(generate_rsa_test_vectors) { -expected); TEST(dnssec_rsa_verify_raw) { -#if !defined(OPENSSL_NO_DEPRECATED_3_0) uint8_t *p; TEST_RSA_VERIFY(test_signature, test_digest, test_exponent, test_modulus, 1); @@ -245,9 +244,6 @@ TEST(dnssec_rsa_verify_raw) { p[bad_modulus.iov_len - 1] ^= 0x01; bad_modulus.iov_len -= 1; TEST_RSA_VERIFY(test_signature, test_digest, test_exponent, bad_modulus, -EINVAL); -#else - TEST_RSA_VERIFY(test_signature, test_digest, test_exponent, test_modulus, -EOPNOTSUPP); -#endif } static int intro(void) { From 59fa4dd142fbc8a8c3165a17faca633e4e83bad8 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 20:15:31 +0900 Subject: [PATCH 08/71] resolve: add unit test for dnssec_ecdsa_verify_raw() --- src/resolve/resolved-dns-dnssec-crypto.h | 8 ++ src/resolve/resolved-dns-dnssec.c | 2 +- src/resolve/test-dnssec-crypto.c | 156 +++++++++++++++++++++++ 3 files changed, 165 insertions(+), 1 deletion(-) diff --git a/src/resolve/resolved-dns-dnssec-crypto.h b/src/resolve/resolved-dns-dnssec-crypto.h index c0b00e6ae2a16..568e511d18954 100644 --- a/src/resolve/resolved-dns-dnssec-crypto.h +++ b/src/resolve/resolved-dns-dnssec-crypto.h @@ -13,4 +13,12 @@ int dnssec_rsa_verify_raw( const struct iovec *exponent, const struct iovec *modulus); +int dnssec_ecdsa_verify_raw( + const EVP_MD *hash_algorithm, + int curve, + const void *signature_r, size_t signature_r_size, + const void *signature_s, size_t signature_s_size, + const void *data, size_t data_size, + const void *key, size_t key_size); + #endif diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index b9afdac9ffd4f..b7748272f3395 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -204,7 +204,7 @@ static int dnssec_rsa_verify( &IOVEC_MAKE(modulus, modulus_size)); } -static int dnssec_ecdsa_verify_raw( +int dnssec_ecdsa_verify_raw( const EVP_MD *hash_algorithm, int curve, const void *signature_r, size_t signature_r_size, diff --git a/src/resolve/test-dnssec-crypto.c b/src/resolve/test-dnssec-crypto.c index 034320b348d8b..ff22f5ddd9d70 100644 --- a/src/resolve/test-dnssec-crypto.c +++ b/src/resolve/test-dnssec-crypto.c @@ -246,6 +246,162 @@ TEST(dnssec_rsa_verify_raw) { TEST_RSA_VERIFY(test_signature, test_digest, test_exponent, bad_modulus, -EINVAL); } +static const uint8_t test_ecdsa_r_buf[] = { + 0x0b, 0x81, 0x39, 0x45, 0xa8, 0xcd, 0x1b, 0x12, + 0xd6, 0x9f, 0xa9, 0x88, 0xf2, 0x28, 0x39, 0x13, + 0x59, 0x1d, 0xe7, 0xee, 0xea, 0x92, 0x87, 0x0a, + 0x2c, 0x7a, 0xf2, 0x3b, 0xfb, 0x3e, 0xcb, 0x3b, +}; + +static const uint8_t test_ecdsa_s_buf[] = { + 0x74, 0xcd, 0x7c, 0x0b, 0x52, 0xa5, 0x7b, 0xf5, + 0x63, 0x84, 0xd5, 0xd9, 0xd2, 0xcb, 0xe3, 0xce, + 0x55, 0x05, 0x11, 0x7c, 0xe1, 0xfe, 0x55, 0xb4, + 0x54, 0x2e, 0x6e, 0x1c, 0xff, 0x50, 0xe5, 0x92, +}; + +static const uint8_t test_ecdsa_key_buf[] = { + 0x04, 0x23, 0x1f, 0x7d, 0x08, 0xec, 0x20, 0xcd, + 0xde, 0x03, 0x93, 0xa7, 0xb2, 0x47, 0x73, 0xeb, + 0xde, 0xd3, 0x5a, 0xbe, 0x35, 0x01, 0xda, 0x31, + 0x2b, 0x7b, 0x61, 0xcf, 0xd2, 0x30, 0xbf, 0xbf, + 0xb7, 0x6f, 0x0d, 0x86, 0x0a, 0x32, 0x46, 0xb6, + 0xdc, 0xb0, 0xd0, 0xa7, 0x3f, 0x5d, 0xdd, 0xdd, + 0xb9, 0xbb, 0x6b, 0x01, 0x61, 0x93, 0x2c, 0x1a, + 0x26, 0x65, 0x5f, 0x46, 0xb1, 0xe1, 0xc7, 0x1d, + 0x21, +}; + +static const struct iovec test_ecdsa_r = IOVEC_MAKE(test_ecdsa_r_buf, sizeof(test_ecdsa_r_buf)); +static const struct iovec test_ecdsa_s = IOVEC_MAKE(test_ecdsa_s_buf, sizeof(test_ecdsa_s_buf)); +static const struct iovec test_ecdsa_key = IOVEC_MAKE(test_ecdsa_key_buf, sizeof(test_ecdsa_key_buf)); + +TEST(generate_ecdsa_test_vectors) { + /* This does not test anything but generates test vectors for dnssec_ecdsa_verify_raw(). + * This is skipped when we are running on valgrind or sanitizers, as it is extremely slow. */ +#if HAVE_VALGRIND_VALGRIND_H + if (RUNNING_ON_VALGRIND) + return (void) log_tests_skipped("Running on valgrind"); +#endif +#if HAS_FEATURE_ADDRESS_SANITIZER + return (void) log_tests_skipped("Running on sanitizers"); +#endif + + /* Generate a P-256 (prime256v1) EC key pair. */ + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *kctx = + ASSERT_NOT_NULL(sym_EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_keygen_init(kctx)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_CTX_set_ec_paramgen_curve_nid(kctx, NID_X9_62_prime256v1)); + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; + ASSERT_OK_POSITIVE(sym_EVP_PKEY_generate(kctx, &pkey)); + + /* Export uncompressed public key point (0x04 || X || Y) */ + size_t key_sz = 0; + ASSERT_OK_POSITIVE(sym_EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, NULL, 0, &key_sz)); + struct iovec pubkey = IOVEC_ALLOCA(key_sz); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, pubkey.iov_base, key_sz, &key_sz)); + pubkey.iov_len = key_sz; + + /* Sign the existing test_digest with ECDSA */ + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *sctx = ASSERT_NOT_NULL(sym_EVP_PKEY_CTX_new(pkey, NULL)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_sign_init(sctx)); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_CTX_set_signature_md(sctx, sym_EVP_sha256())); + size_t sig_sz; + ASSERT_OK_POSITIVE(sym_EVP_PKEY_sign(sctx, NULL, &sig_sz, test_digest.iov_base, test_digest.iov_len)); + struct iovec signature_der = IOVEC_ALLOCA(sig_sz); + ASSERT_OK_POSITIVE(sym_EVP_PKEY_sign(sctx, signature_der.iov_base, &signature_der.iov_len, test_digest.iov_base, test_digest.iov_len)); + + /* ECDSA signature is DER encoded. Extract r and s components. */ + const uint8_t *p = signature_der.iov_base; + _cleanup_(ECDSA_SIG_freep) ECDSA_SIG *esig = ASSERT_NOT_NULL(sym_d2i_ECDSA_SIG(NULL, &p, signature_der.iov_len)); + + const BIGNUM *r_bn = sym_ECDSA_SIG_get0_r(esig); + const BIGNUM *s_bn = sym_ECDSA_SIG_get0_s(esig); + + _cleanup_(iovec_done) struct iovec r_iov = {}, s_iov = {}; + export_bn(r_bn, &r_iov); + export_bn(s_bn, &s_iov); + + iovec_dump_c("test_ecdsa_r_buf", &r_iov); + iovec_dump_c("test_ecdsa_s_buf", &s_iov); + iovec_dump_c("test_ecdsa_key_buf", &pubkey); +} + +#define TEST_ECDSA_VERIFY(r, s, digest, key, expected) \ + if (expected >= 0) \ + ASSERT_OK_EQ(dnssec_ecdsa_verify_raw( \ + sym_EVP_sha256(), \ + NID_X9_62_prime256v1, \ + (r).iov_base, (r).iov_len, \ + (s).iov_base, (s).iov_len, \ + (digest).iov_base, (digest).iov_len, \ + (key).iov_base, (key).iov_len), \ + expected); \ + else \ + ASSERT_ERROR(dnssec_ecdsa_verify_raw( \ + sym_EVP_sha256(), \ + NID_X9_62_prime256v1, \ + (r).iov_base, (r).iov_len, \ + (s).iov_base, (s).iov_len, \ + (digest).iov_base, (digest).iov_len, \ + (key).iov_base, (key).iov_len), \ + -expected); + +TEST(dnssec_ecdsa_verify_raw) { +#if !defined(OPENSSL_NO_DEPRECATED_3_0) + uint8_t *p; + + /* Normal verification */ + TEST_ECDSA_VERIFY(test_ecdsa_r, test_ecdsa_s, test_digest, test_ecdsa_key, 1); + + /* Fuzzing R component */ + _cleanup_(iovec_done) struct iovec bad_r = {}; + ASSERT_NOT_NULL(iovec_memdup(&test_ecdsa_r, &bad_r)); + p = bad_r.iov_base; + p[0] ^= 0x01; + TEST_ECDSA_VERIFY(bad_r, test_ecdsa_s, test_digest, test_ecdsa_key, 0); + + p[0] ^= 0x01; + bad_r.iov_len -= 1; + TEST_ECDSA_VERIFY(bad_r, test_ecdsa_s, test_digest, test_ecdsa_key, 0); + + /* Fuzzing S component */ + _cleanup_(iovec_done) struct iovec bad_s = {}; + ASSERT_NOT_NULL(iovec_memdup(&test_ecdsa_s, &bad_s)); + p = bad_s.iov_base; + p[0] ^= 0x01; + TEST_ECDSA_VERIFY(test_ecdsa_r, bad_s, test_digest, test_ecdsa_key, 0); + + p[0] ^= 0x01; + bad_s.iov_len -= 1; + TEST_ECDSA_VERIFY(test_ecdsa_r, bad_s, test_digest, test_ecdsa_key, 0); + + /* Fuzzing Digest */ + _cleanup_(iovec_done) struct iovec bad_digest = {}; + ASSERT_NOT_NULL(iovec_memdup(&test_digest, &bad_digest)); + p = bad_digest.iov_base; + p[0] ^= 0x01; + TEST_ECDSA_VERIFY(test_ecdsa_r, test_ecdsa_s, bad_digest, test_ecdsa_key, 0); + + p[0] ^= 0x01; + bad_digest.iov_len -= 1; + TEST_ECDSA_VERIFY(test_ecdsa_r, test_ecdsa_s, bad_digest, test_ecdsa_key, 0); + + /* Fuzzing Public Key Point */ + _cleanup_(iovec_done) struct iovec bad_key = {}; + ASSERT_NOT_NULL(iovec_memdup(&test_ecdsa_key, &bad_key)); + p = bad_key.iov_base; + p[bad_key.iov_len - 1] ^= 0x01; + TEST_ECDSA_VERIFY(test_ecdsa_r, test_ecdsa_s, test_digest, bad_key, -ENOTRECOVERABLE); + + p[bad_key.iov_len - 1] ^= 0x01; + bad_key.iov_len -= 1; + TEST_ECDSA_VERIFY(test_ecdsa_r, test_ecdsa_s, test_digest, bad_key, -ENOTRECOVERABLE); +#else + TEST_ECDSA_VERIFY(test_ecdsa_r, test_ecdsa_s, test_digest, test_ecdsa_key, -EOPNOTSUPP); +#endif +} + static int intro(void) { if (DLOPEN_LIBCRYPTO(LOG_DEBUG, SD_ELF_NOTE_DLOPEN_PRIORITY_RECOMMENDED) < 0) return EXIT_TEST_SKIP; From 13be55350746554a1b5a0f26c5c5c21e7e538087 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 20:16:00 +0900 Subject: [PATCH 09/71] resolve: make dnssec_ecdsa_verify_raw() take struct iovec This also - adds missing assertions, - moves variable declarations, - rename variables. No functional change, just refactoring. --- src/resolve/resolved-dns-dnssec-crypto.h | 8 +-- src/resolve/resolved-dns-dnssec.c | 70 ++++++++++++------------ src/resolve/test-dnssec-crypto.c | 10 +--- 3 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/resolve/resolved-dns-dnssec-crypto.h b/src/resolve/resolved-dns-dnssec-crypto.h index 568e511d18954..87dd6c8db5ea4 100644 --- a/src/resolve/resolved-dns-dnssec-crypto.h +++ b/src/resolve/resolved-dns-dnssec-crypto.h @@ -16,9 +16,9 @@ int dnssec_rsa_verify_raw( int dnssec_ecdsa_verify_raw( const EVP_MD *hash_algorithm, int curve, - const void *signature_r, size_t signature_r_size, - const void *signature_s, size_t signature_s_size, - const void *data, size_t data_size, - const void *key, size_t key_size); + const struct iovec *signature_r, + const struct iovec *signature_s, + const struct iovec *hash, + const struct iovec *key); #endif diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index b7748272f3395..b2e04b3928b0b 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -207,40 +207,37 @@ static int dnssec_rsa_verify( int dnssec_ecdsa_verify_raw( const EVP_MD *hash_algorithm, int curve, - const void *signature_r, size_t signature_r_size, - const void *signature_s, size_t signature_s_size, - const void *data, size_t data_size, - const void *key, size_t key_size) { + const struct iovec *signature_r, + const struct iovec *signature_s, + const struct iovec *hash, + const struct iovec *key) { #if !defined(OPENSSL_NO_DEPRECATED_3_0) DISABLE_WARNING_DEPRECATED_DECLARATIONS; - int k; - - _cleanup_(EC_GROUP_freep) EC_GROUP *ec_group = NULL; - _cleanup_(EC_POINT_freep) EC_POINT *p = NULL; - _cleanup_(EC_KEY_freep) EC_KEY *eckey = NULL; - _cleanup_(BN_CTX_freep) BN_CTX *bctx = NULL; - _cleanup_(BN_freep) BIGNUM *r = NULL, *s = NULL; - _cleanup_(ECDSA_SIG_freep) ECDSA_SIG *sig = NULL; + int r; assert(hash_algorithm); + assert(iovec_is_set(signature_r)); + assert(iovec_is_set(signature_s)); + assert(iovec_is_set(hash)); + assert(iovec_is_set(key)); - ec_group = sym_EC_GROUP_new_by_curve_name(curve); + _cleanup_(EC_GROUP_freep) EC_GROUP *ec_group = sym_EC_GROUP_new_by_curve_name(curve); if (!ec_group) return -ENOMEM; - p = sym_EC_POINT_new(ec_group); + _cleanup_(EC_POINT_freep) EC_POINT *p = sym_EC_POINT_new(ec_group); if (!p) return -ENOMEM; - bctx = sym_BN_CTX_new(); + _cleanup_(BN_CTX_freep) BN_CTX *bctx = sym_BN_CTX_new(); if (!bctx) return -ENOMEM; - if (sym_EC_POINT_oct2point(ec_group, p, key, key_size, bctx) <= 0) + if (sym_EC_POINT_oct2point(ec_group, p, key->iov_base, key->iov_len, bctx) <= 0) return log_openssl_errors(LOG_DEBUG, "Failed to parse EC public key point"); - eckey = sym_EC_KEY_new(); + _cleanup_(EC_KEY_freep) EC_KEY *eckey = sym_EC_KEY_new(); if (!eckey) return -ENOMEM; @@ -250,33 +247,34 @@ int dnssec_ecdsa_verify_raw( if (sym_EC_KEY_set_public_key(eckey, p) <= 0) return log_openssl_errors(LOG_DEBUG, "EC_KEY_set_public_key failed"); - if (sym_EC_KEY_check_key(eckey) != 1) + if (sym_EC_KEY_check_key(eckey) <= 0) return log_openssl_errors(LOG_DEBUG, "EC_KEY_check_key failed"); - r = sym_BN_bin2bn(signature_r, signature_r_size, NULL); - if (!r) + _cleanup_(BN_freep) BIGNUM *bn_r = sym_BN_bin2bn(signature_r->iov_base, signature_r->iov_len, NULL); + if (!bn_r) return log_openssl_errors(LOG_DEBUG, "Failed to convert ECDSA signature r to BIGNUM"); - s = sym_BN_bin2bn(signature_s, signature_s_size, NULL); - if (!s) + _cleanup_(BN_freep) BIGNUM *bn_s = sym_BN_bin2bn(signature_s->iov_base, signature_s->iov_len, NULL); + if (!bn_s) return log_openssl_errors(LOG_DEBUG, "Failed to convert ECDSA signature s to BIGNUM"); /* TODO: We should eventually use the EVP API once it supports ECDSA signature verification */ - sig = sym_ECDSA_SIG_new(); + _cleanup_(ECDSA_SIG_freep) ECDSA_SIG *sig = sym_ECDSA_SIG_new(); if (!sig) return -ENOMEM; - if (sym_ECDSA_SIG_set0(sig, r, s) <= 0) + if (sym_ECDSA_SIG_set0(sig, bn_r, bn_s) <= 0) return log_openssl_errors(LOG_DEBUG, "Failed to set ECDSA signature"); - r = s = NULL; + TAKE_PTR(bn_r); + TAKE_PTR(bn_s); - k = sym_ECDSA_do_verify(data, data_size, sig, eckey); - if (k < 0) + r = sym_ECDSA_do_verify(hash->iov_base, hash->iov_len, sig, eckey); + if (r < 0) return log_openssl_errors(LOG_DEBUG, "Signature verification failed"); REENABLE_WARNING; - return k; + return r; #else return -EOPNOTSUPP; #endif @@ -285,7 +283,7 @@ int dnssec_ecdsa_verify_raw( static int dnssec_ecdsa_verify( const EVP_MD *hash_algorithm, int algorithm, - const void *hash, size_t hash_size, + const struct iovec *hash, DnsResourceRecord *rrsig, DnsResourceRecord *dnskey) { @@ -293,8 +291,8 @@ static int dnssec_ecdsa_verify( size_t key_size; uint8_t *q; - assert(hash); - assert(hash_size); + assert(hash_algorithm); + assert(iovec_is_set(hash)); assert(rrsig); assert(dnskey); @@ -320,10 +318,10 @@ static int dnssec_ecdsa_verify( return dnssec_ecdsa_verify_raw( hash_algorithm, curve, - rrsig->rrsig.signature, key_size, - (uint8_t*) rrsig->rrsig.signature + key_size, key_size, - hash, hash_size, - q, key_size*2+1); + &IOVEC_MAKE(rrsig->rrsig.signature, key_size), + &IOVEC_MAKE((uint8_t*) rrsig->rrsig.signature + key_size, key_size), + hash, + &IOVEC_MAKE(q, key_size * 2 + 1)); } static int dnssec_eddsa_verify_raw( @@ -698,7 +696,7 @@ static int dnssec_rrset_verify_sig( return dnssec_verify_errno(dnssec_ecdsa_verify( md_algorithm, rrsig->rrsig.algorithm, - hash, hash_size, + &IOVEC_MAKE(hash, hash_size), rrsig, dnskey)); diff --git a/src/resolve/test-dnssec-crypto.c b/src/resolve/test-dnssec-crypto.c index ff22f5ddd9d70..47dff4af699fb 100644 --- a/src/resolve/test-dnssec-crypto.c +++ b/src/resolve/test-dnssec-crypto.c @@ -332,19 +332,13 @@ TEST(generate_ecdsa_test_vectors) { ASSERT_OK_EQ(dnssec_ecdsa_verify_raw( \ sym_EVP_sha256(), \ NID_X9_62_prime256v1, \ - (r).iov_base, (r).iov_len, \ - (s).iov_base, (s).iov_len, \ - (digest).iov_base, (digest).iov_len, \ - (key).iov_base, (key).iov_len), \ + &r, &s, &digest, &key), \ expected); \ else \ ASSERT_ERROR(dnssec_ecdsa_verify_raw( \ sym_EVP_sha256(), \ NID_X9_62_prime256v1, \ - (r).iov_base, (r).iov_len, \ - (s).iov_base, (s).iov_len, \ - (digest).iov_base, (digest).iov_len, \ - (key).iov_base, (key).iov_len), \ + &r, &s, &digest, &key), \ -expected); TEST(dnssec_ecdsa_verify_raw) { From c18ee628a502b5694caade7482be85d0e7cc0284 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 21:05:50 +0900 Subject: [PATCH 10/71] resolved: migrate ECDSA verification to OpenSSL 3 EVP API OpenSSL 3.0 deprecated low-level Elliptic Curve (EC) key manipulation functions (EC_KEY, EC_POINT, EC_GROUP) and direct signature verification functions like ECDSA_do_verify(). This commit modernizes dnssec_ecdsa_verify_raw() by transitioning to the provider-aware EVP API: * Uses OSSL_PARAM arrays and EVP_PKEY_fromdata() to construct the EC public key directly from the raw octet string and curve name, avoiding deprecated EC_POINT parsing. * Converts the raw R and S signature components into a DER-encoded ASN.1 signature using i2d_ECDSA_SIG(), as required by the modern EVP API. * Uses EVP_PKEY_verify() for the actual signature validation. Additionally, this drops an outdated TODO comment waiting for raw ECDSA support in the EVP API, as well as the deprecated warning suppression macros and fallback code blocks. Unit tests for ECDSA are now run unconditionally. --- src/resolve/resolved-dns-dnssec.c | 66 ++++++++++++++++--------------- src/resolve/test-dnssec-crypto.c | 4 -- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index b2e04b3928b0b..67cf991e70596 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -212,8 +212,6 @@ int dnssec_ecdsa_verify_raw( const struct iovec *hash, const struct iovec *key) { -#if !defined(OPENSSL_NO_DEPRECATED_3_0) - DISABLE_WARNING_DEPRECATED_DECLARATIONS; int r; assert(hash_algorithm); @@ -222,33 +220,26 @@ int dnssec_ecdsa_verify_raw( assert(iovec_is_set(hash)); assert(iovec_is_set(key)); - _cleanup_(EC_GROUP_freep) EC_GROUP *ec_group = sym_EC_GROUP_new_by_curve_name(curve); - if (!ec_group) - return -ENOMEM; - - _cleanup_(EC_POINT_freep) EC_POINT *p = sym_EC_POINT_new(ec_group); - if (!p) - return -ENOMEM; + const char *curve_name = sym_OBJ_nid2sn(curve); + if (!curve_name) + return log_openssl_errors(LOG_DEBUG, "Unknown curve NID"); - _cleanup_(BN_CTX_freep) BN_CTX *bctx = sym_BN_CTX_new(); - if (!bctx) - return -ENOMEM; - - if (sym_EC_POINT_oct2point(ec_group, p, key->iov_base, key->iov_len, bctx) <= 0) - return log_openssl_errors(LOG_DEBUG, "Failed to parse EC public key point"); + OSSL_PARAM params[] = { + sym_OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, (char*) curve_name, 0), + sym_OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_PUB_KEY, key->iov_base, key->iov_len), + sym_OSSL_PARAM_construct_end(), + }; - _cleanup_(EC_KEY_freep) EC_KEY *eckey = sym_EC_KEY_new(); - if (!eckey) + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *kctx = sym_EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL); + if (!kctx) return -ENOMEM; - if (sym_EC_KEY_set_group(eckey, ec_group) <= 0) - return log_openssl_errors(LOG_DEBUG, "Failed to set EC group"); - - if (sym_EC_KEY_set_public_key(eckey, p) <= 0) - return log_openssl_errors(LOG_DEBUG, "EC_KEY_set_public_key failed"); + if (sym_EVP_PKEY_fromdata_init(kctx) <= 0) + return log_openssl_errors(LOG_DEBUG, "Failed to initialize EC key creation"); - if (sym_EC_KEY_check_key(eckey) <= 0) - return log_openssl_errors(LOG_DEBUG, "EC_KEY_check_key failed"); + _cleanup_(EVP_PKEY_freep) EVP_PKEY *epubkey = NULL; + if (sym_EVP_PKEY_fromdata(kctx, &epubkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) + return log_openssl_errors(LOG_DEBUG, "Failed to load EC public key from raw data"); _cleanup_(BN_freep) BIGNUM *bn_r = sym_BN_bin2bn(signature_r->iov_base, signature_r->iov_len, NULL); if (!bn_r) @@ -258,8 +249,6 @@ int dnssec_ecdsa_verify_raw( if (!bn_s) return log_openssl_errors(LOG_DEBUG, "Failed to convert ECDSA signature s to BIGNUM"); - /* TODO: We should eventually use the EVP API once it supports ECDSA signature verification */ - _cleanup_(ECDSA_SIG_freep) ECDSA_SIG *sig = sym_ECDSA_SIG_new(); if (!sig) return -ENOMEM; @@ -269,15 +258,30 @@ int dnssec_ecdsa_verify_raw( TAKE_PTR(bn_r); TAKE_PTR(bn_s); - r = sym_ECDSA_do_verify(hash->iov_base, hash->iov_len, sig, eckey); + _cleanup_(OPENSSL_freep) void *buf = NULL; + r = sym_i2d_ECDSA_SIG(sig, (unsigned char**) &buf); + if (r <= 0) + return log_openssl_errors(LOG_DEBUG, "Failed to DER encode ECDSA signature"); + struct iovec der_sig = IOVEC_MAKE(buf, r); + + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *vctx = sym_EVP_PKEY_CTX_new(epubkey, NULL); + if (!vctx) + return -ENOMEM; + + if (sym_EVP_PKEY_public_check(vctx) <= 0) + return log_openssl_errors(LOG_DEBUG, "EC public key validation failed"); + + if (sym_EVP_PKEY_verify_init(vctx) <= 0) + return log_openssl_errors(LOG_DEBUG, "Failed to initialize ECDSA signature verification"); + + if (sym_EVP_PKEY_CTX_set_signature_md(vctx, hash_algorithm) <= 0) + return log_openssl_errors(LOG_DEBUG, "Failed to set ECDSA signature digest"); + + r = sym_EVP_PKEY_verify(vctx, der_sig.iov_base, der_sig.iov_len, hash->iov_base, hash->iov_len); if (r < 0) return log_openssl_errors(LOG_DEBUG, "Signature verification failed"); - REENABLE_WARNING; return r; -#else - return -EOPNOTSUPP; -#endif } static int dnssec_ecdsa_verify( diff --git a/src/resolve/test-dnssec-crypto.c b/src/resolve/test-dnssec-crypto.c index 47dff4af699fb..82a91a6386c26 100644 --- a/src/resolve/test-dnssec-crypto.c +++ b/src/resolve/test-dnssec-crypto.c @@ -342,7 +342,6 @@ TEST(generate_ecdsa_test_vectors) { -expected); TEST(dnssec_ecdsa_verify_raw) { -#if !defined(OPENSSL_NO_DEPRECATED_3_0) uint8_t *p; /* Normal verification */ @@ -391,9 +390,6 @@ TEST(dnssec_ecdsa_verify_raw) { p[bad_key.iov_len - 1] ^= 0x01; bad_key.iov_len -= 1; TEST_ECDSA_VERIFY(test_ecdsa_r, test_ecdsa_s, test_digest, bad_key, -ENOTRECOVERABLE); -#else - TEST_ECDSA_VERIFY(test_ecdsa_r, test_ecdsa_s, test_digest, test_ecdsa_key, -EOPNOTSUPP); -#endif } static int intro(void) { From ec35f207b6939a3035a70cf30bb0442d2bec258b Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 21:14:47 +0900 Subject: [PATCH 11/71] crypto-util: drop unused deprecated symbols --- src/shared/crypto-util.c | 31 ------------------------------- src/shared/crypto-util.h | 21 --------------------- 2 files changed, 52 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index 281085688898c..68455abc64ca8 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -327,23 +327,6 @@ REENABLE_WARNING; DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(ENGINE*, sym_ENGINE_free, ENGINE_freep, NULL); #endif -#if !defined(OPENSSL_NO_DEPRECATED_3_0) -DISABLE_WARNING_DEPRECATED_DECLARATIONS; -DLSYM_PROTOTYPE(ECDSA_do_verify) = NULL; -DLSYM_PROTOTYPE(EC_KEY_check_key) = NULL; -DLSYM_PROTOTYPE(EC_KEY_free) = NULL; -DLSYM_PROTOTYPE(EC_KEY_new) = NULL; -DLSYM_PROTOTYPE(EC_KEY_set_group) = NULL; -DLSYM_PROTOTYPE(EC_KEY_set_public_key) = NULL; -DLSYM_PROTOTYPE(EVP_PKEY_assign) = NULL; -DLSYM_PROTOTYPE(RSAPublicKey_dup) = NULL; -DLSYM_PROTOTYPE(RSA_free) = NULL; -DLSYM_PROTOTYPE(RSA_new) = NULL; -DLSYM_PROTOTYPE(RSA_set0_key) = NULL; -DLSYM_PROTOTYPE(RSA_size) = NULL; -REENABLE_WARNING; -#endif - #ifndef OPENSSL_NO_UI_CONSOLE static DLSYM_PROTOTYPE(UI_OpenSSL) = NULL; static DLSYM_PROTOTYPE(UI_create_method) = NULL; @@ -656,20 +639,6 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG_FORCE(ENGINE_init), DLSYM_ARG_FORCE(ENGINE_load_private_key), #endif -#if !defined(OPENSSL_NO_DEPRECATED_3_0) - DLSYM_ARG_FORCE(ECDSA_do_verify), - DLSYM_ARG_FORCE(EC_KEY_check_key), - DLSYM_ARG_FORCE(EC_KEY_free), - DLSYM_ARG_FORCE(EC_KEY_new), - DLSYM_ARG_FORCE(EC_KEY_set_group), - DLSYM_ARG_FORCE(EC_KEY_set_public_key), - DLSYM_ARG_FORCE(EVP_PKEY_assign), - DLSYM_ARG_FORCE(RSAPublicKey_dup), - DLSYM_ARG_FORCE(RSA_free), - DLSYM_ARG_FORCE(RSA_new), - DLSYM_ARG_FORCE(RSA_set0_key), - DLSYM_ARG_FORCE(RSA_size), -#endif #ifndef OPENSSL_NO_UI_CONSOLE DLSYM_ARG(UI_OpenSSL), DLSYM_ARG(UI_create_method), diff --git a/src/shared/crypto-util.h b/src/shared/crypto-util.h index 6ecf66bbf65df..b0358090a7797 100644 --- a/src/shared/crypto-util.h +++ b/src/shared/crypto-util.h @@ -283,26 +283,6 @@ extern DLSYM_PROTOTYPE(i2d_PUBKEY); extern DLSYM_PROTOTYPE(i2d_X509); extern DLSYM_PROTOTYPE(i2d_X509_NAME); -#if !defined(OPENSSL_NO_DEPRECATED_3_0) -DISABLE_WARNING_DEPRECATED_DECLARATIONS; -extern DLSYM_PROTOTYPE(ECDSA_do_verify); -extern DLSYM_PROTOTYPE(EC_KEY_check_key); -extern DLSYM_PROTOTYPE(EC_KEY_free); -extern DLSYM_PROTOTYPE(EC_KEY_new); -extern DLSYM_PROTOTYPE(EC_KEY_set_group); -extern DLSYM_PROTOTYPE(EC_KEY_set_public_key); -extern DLSYM_PROTOTYPE(EVP_PKEY_assign); -extern DLSYM_PROTOTYPE(RSAPublicKey_dup); -extern DLSYM_PROTOTYPE(RSA_free); -extern DLSYM_PROTOTYPE(RSA_new); -extern DLSYM_PROTOTYPE(RSA_set0_key); -extern DLSYM_PROTOTYPE(RSA_size); -REENABLE_WARNING; - -DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(EC_KEY*, sym_EC_KEY_free, EC_KEY_freep, NULL); -DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(RSA*, sym_RSA_free, RSA_freep, NULL); -#endif - /* Mirrors of OpenSSL macros that go through our dlopen'd sym_* variants, so we don't end up linking against * libcrypto just for these. */ #define sym_BIO_get_md_ctx(b, mdcp) sym_BIO_ctrl((b), BIO_C_GET_MD_CTX, 0, (char*) (mdcp)) @@ -312,7 +292,6 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(RSA*, sym_RSA_free, RSA_freep, NULL); #define sym_BN_one(a) sym_BN_set_word(a, 1) #define sym_EVP_MD_CTX_get_size(ctx) sym_EVP_MD_get_size(sym_EVP_MD_CTX_get0_md(ctx)) #define sym_EVP_MD_CTX_get0_name(ctx) sym_EVP_MD_get0_name(sym_EVP_MD_CTX_get0_md(ctx)) -#define sym_EVP_PKEY_assign_RSA(pkey, rsa) sym_EVP_PKEY_assign((pkey), EVP_PKEY_RSA, (rsa)) #define sym_OPENSSL_free(addr) sym_CRYPTO_free((addr), OPENSSL_FILE, OPENSSL_LINE) #define sym_PKCS7_set_detached(p, v) sym_PKCS7_ctrl((p), PKCS7_OP_SET_DETACHED_SIGNATURE, (v), NULL) From c0c6a9e8e477c79370afd2415785f5c33f05ff43 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 30 Jun 2026 23:56:43 +0900 Subject: [PATCH 12/71] crypto-util: simplify pubkey_fingerprint() There is no need to call i2d_PublicKey() twice. Passing a pointer to NULL allows OpenSSL to automatically allocate the necessary buffer. --- src/shared/crypto-util.c | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index 68455abc64ca8..f99147d7bb046 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -1635,10 +1635,9 @@ int ecc_ecdh(const EVP_PKEY *private_pkey, int pubkey_fingerprint(EVP_PKEY *pk, const EVP_MD *md, void **ret, size_t *ret_size) { _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX* m = NULL; - _cleanup_free_ void *d = NULL, *h = NULL; - int sz, lsz, msz; + _cleanup_free_ void *h = NULL; + int lsz, msz; unsigned umsz; - unsigned char *dd; int r; /* Calculates a message digest of the DER encoded public key */ @@ -1652,15 +1651,8 @@ int pubkey_fingerprint(EVP_PKEY *pk, const EVP_MD *md, void **ret, size_t *ret_s if (r < 0) return r; - sz = sym_i2d_PublicKey(pk, NULL); - if (sz < 0) - return log_openssl_errors(LOG_DEBUG, "Unable to convert public key to DER format"); - - dd = d = malloc(sz); - if (!d) - return log_oom_debug(); - - lsz = sym_i2d_PublicKey(pk, &dd); + _cleanup_(OPENSSL_freep) void *d = NULL; + lsz = sym_i2d_PublicKey(pk, (unsigned char**) &d); if (lsz < 0) return log_openssl_errors(LOG_DEBUG, "Unable to convert public key to DER format"); From 94e8d7209ef86892e6b6d045e25d875528ea1d0d Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 1 Jul 2026 00:00:11 +0900 Subject: [PATCH 13/71] crypto-util: use correct cleanup function for OpenSSL buffers Buffers allocated by OpenSSL must be freed with OPENSSL_free(). Fortunately, we do not enable the secure heap, so OPENSSL_free() is currently equivalent to free(), but let's fix this for correctness. --- src/shared/crypto-util.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index f99147d7bb046..006edbe8662dd 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -1860,7 +1860,6 @@ static int ecc_pkey_generate_volume_keys( _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey_new = NULL; _cleanup_(erase_and_freep) void *decrypted_key = NULL; - _cleanup_free_ unsigned char *saved_key = NULL; size_t decrypted_key_size, saved_key_size; int r; @@ -1892,10 +1891,17 @@ static int ecc_pkey_generate_volume_keys( /* EVP_PKEY_get1_encoded_public_key() always returns uncompressed format of EC points. See https://github.com/openssl/openssl/discussions/22835 */ - saved_key_size = sym_EVP_PKEY_get1_encoded_public_key(pkey_new, &saved_key); + _cleanup_(OPENSSL_freep) void *buf = NULL; + saved_key_size = sym_EVP_PKEY_get1_encoded_public_key(pkey_new, (unsigned char**) &buf); if (saved_key_size == 0) return log_openssl_errors(LOG_DEBUG, "Failed to convert the generated public key to SEC1 format"); + /* 'buf' is allocated by OpenSSL and must be freed via OPENSSL_free(). We duplicate it here so the + * caller can safely use standard free(). */ + _cleanup_free_ void *saved_key = memdup(buf, saved_key_size); + if (!saved_key) + return log_oom_debug(); + *ret_decrypted_key = TAKE_PTR(decrypted_key); *ret_decrypted_key_size = decrypted_key_size; *ret_saved_key = TAKE_PTR(saved_key); @@ -2278,7 +2284,7 @@ OpenSSLAskPasswordUI* openssl_ask_password_ui_free(OpenSSLAskPasswordUI *ui) { } int x509_fingerprint(X509 *cert, uint8_t buffer[static SHA256_DIGEST_SIZE]) { - _cleanup_free_ uint8_t *der = NULL; + _cleanup_(OPENSSL_freep) void *der = NULL; int dersz, r; assert(cert); @@ -2287,7 +2293,7 @@ int x509_fingerprint(X509 *cert, uint8_t buffer[static SHA256_DIGEST_SIZE]) { if (r < 0) return r; - dersz = sym_i2d_X509(cert, &der); + dersz = sym_i2d_X509(cert, (unsigned char**) &der); if (dersz < 0) return log_openssl_errors(LOG_DEBUG, "Unable to convert PEM certificate to DER format"); From fc54be2f666ef09a8b85edc41c094ff23adf8558 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 1 Jul 2026 00:33:27 +0900 Subject: [PATCH 14/71] crypto-util: simplify openssl_extract_public_key() Drop memstream and i2d_PUBKEY_fp(). We can simply use i2d_PUBKEY() which automatically allocates the necessary buffer for us. Note that dropping the secure erase (erase_and_freep()) in favor of OPENSSL_free() is intentional and safe, as the buffer only holds public key material which does not need to be securely wiped. --- src/shared/crypto-util.c | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index 006edbe8662dd..ac4737afc09a4 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -306,7 +306,6 @@ DLSYM_PROTOTYPE(i2d_ECDSA_SIG) = NULL; DLSYM_PROTOTYPE(i2d_PKCS7) = NULL; DLSYM_PROTOTYPE(i2d_PKCS7_fp) = NULL; DLSYM_PROTOTYPE(i2d_PUBKEY) = NULL; -static DLSYM_PROTOTYPE(i2d_PUBKEY_fp) = NULL; static DLSYM_PROTOTYPE(i2d_PublicKey) = NULL; DLSYM_PROTOTYPE(i2d_X509) = NULL; DLSYM_PROTOTYPE(i2d_X509_NAME) = NULL; @@ -629,7 +628,6 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG(i2d_PKCS7), DLSYM_ARG(i2d_PKCS7_fp), DLSYM_ARG(i2d_PUBKEY), - DLSYM_ARG(i2d_PUBKEY_fp), DLSYM_ARG(i2d_PublicKey), DLSYM_ARG(i2d_X509), DLSYM_ARG(i2d_X509_NAME), @@ -2401,21 +2399,12 @@ int openssl_extract_public_key(EVP_PKEY *private_key, EVP_PKEY **ret) { if (r < 0) return r; - _cleanup_(memstream_done) MemStream m = {}; - FILE *tf = memstream_init(&m); - if (!tf) - return -ENOMEM; - - if (sym_i2d_PUBKEY_fp(tf, private_key) != 1) + _cleanup_(OPENSSL_freep) void *buf = NULL; + int len = sym_i2d_PUBKEY(private_key, (unsigned char**) &buf); + if (len < 0) return log_openssl_errors(LOG_DEBUG, "Failed to extract public key in DER format"); - _cleanup_(erase_and_freep) char *buf = NULL; - size_t len; - r = memstream_finalize(&m, &buf, &len); - if (r < 0) - return r; - - const unsigned char *t = (const unsigned char*) buf; + const unsigned char *t = buf; if (!sym_d2i_PUBKEY(ret, &t, len)) return log_openssl_errors(LOG_DEBUG, "Failed to parse public key in DER format"); From b5e75134f380d66c3d4d173b05fca82b770a1454 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 1 Jul 2026 00:39:53 +0900 Subject: [PATCH 15/71] crypto-util: drop manual endianness handling in rsa_pkey_from_n_e() Currently, rsa_pkey_from_n_e() uses architecture-specific `#if` branches and memdup_reverse() to handle big-endian RSA components (n and e) before passing them directly to OSSL_PARAM_construct_BN(). We can simplify this by parsing the raw big-endian bytes into BIGNUMs first using BN_bin2bn(), which natively expects big-endian data. We can then push these BIGNUMs into OSSL_PARAM_BLD. This delegates the data format handling entirely to OpenSSL and successfully removes the platform-specific code. --- src/shared/crypto-util.c | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index ac4737afc09a4..8e6c5a9d44934 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -1350,30 +1350,32 @@ int rsa_pkey_from_n_e(const void *n, size_t n_size, const void *e, size_t e_size if (sym_EVP_PKEY_fromdata_init(ctx) <= 0) return log_openssl_errors(LOG_DEBUG, "Failed to initialize EVP_PKEY_CTX"); - OSSL_PARAM params[3]; + _cleanup_(BN_freep) BIGNUM *bn_n = sym_BN_bin2bn(n, n_size, NULL); + if (!bn_n) + return log_openssl_errors(LOG_DEBUG, "Failed to create BIGNUM n"); -#if __BYTE_ORDER == __BIG_ENDIAN - params[0] = sym_OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_RSA_N, (void*)n, n_size); - params[1] = sym_OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_RSA_E, (void*)e, e_size); -#else - _cleanup_free_ void *native_n = memdup_reverse(n, n_size); - if (!native_n) - return log_oom_debug(); + _cleanup_(BN_freep) BIGNUM *bn_e = sym_BN_bin2bn(e, e_size, NULL); + if (!bn_e) + return log_openssl_errors(LOG_DEBUG, "Failed to create BIGNUM e"); - _cleanup_free_ void *native_e = memdup_reverse(e, e_size); - if (!native_e) - return log_oom_debug(); + _cleanup_(OSSL_PARAM_BLD_freep) OSSL_PARAM_BLD *bld = sym_OSSL_PARAM_BLD_new(); + if (!bld) + return log_openssl_errors(LOG_DEBUG, "Failed to create new OSSL_PARAM_BLD"); - params[0] = sym_OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_RSA_N, native_n, n_size); - params[1] = sym_OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_RSA_E, native_e, e_size); -#endif - params[2] = sym_OSSL_PARAM_construct_end(); + if (!sym_OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_N, bn_n)) + return log_openssl_errors(LOG_DEBUG, "Failed to push n to RSA params"); + + if (!sym_OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_E, bn_e)) + return log_openssl_errors(LOG_DEBUG, "Failed to push e to RSA params"); + + _cleanup_(OSSL_PARAM_freep) OSSL_PARAM *params = sym_OSSL_PARAM_BLD_to_param(bld); + if (!params) + return log_openssl_errors(LOG_DEBUG, "Failed to build RSA OSSL_PARAM"); if (sym_EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) return log_openssl_errors(LOG_DEBUG, "Failed to create RSA EVP_PKEY"); *ret = TAKE_PTR(pkey); - return 0; } From 75bc1060950d1762eeec3a2f9049274d58ee0110 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 1 Jul 2026 01:55:39 +0900 Subject: [PATCH 16/71] memory-util: drop unused memdup_reverse() With the previous commit, now the function is unused anymore. Let's drop it. --- src/basic/memory-util.c | 16 ---------------- src/basic/memory-util.h | 3 --- 2 files changed, 19 deletions(-) diff --git a/src/basic/memory-util.c b/src/basic/memory-util.c index f03ed7adbbe26..10a5157ca6466 100644 --- a/src/basic/memory-util.c +++ b/src/basic/memory-util.c @@ -19,22 +19,6 @@ size_t page_size(void) { return pgsz; } -void* memdup_reverse(const void *mem, size_t size) { - assert(mem); - assert(size != 0); - - void *p = malloc(size); - if (!p) - return NULL; - - uint8_t *p_dst = p; - const uint8_t *p_src = mem; - for (size_t i = 0, k = size; i < size; i++, k--) - p_dst[i] = p_src[k-1]; - - return p; -} - void* erase_and_free(void *p) { size_t l; diff --git a/src/basic/memory-util.h b/src/basic/memory-util.h index 4caf58585a779..39dff8271a217 100644 --- a/src/basic/memory-util.h +++ b/src/basic/memory-util.h @@ -98,6 +98,3 @@ static inline void erase_and_freep(void *p) { static inline void erase_char(char *p) { explicit_bzero_safe(p, sizeof(char)); } - -/* Makes a copy of the buffer with reversed order of bytes */ -void* memdup_reverse(const void *mem, size_t size); From 3aaffa43f0a46cb6ba3bf880d4910a12c17fd0ed Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 1 Jul 2026 14:32:24 +0900 Subject: [PATCH 17/71] crypto-util: drop dlopen_libcrypto() from static functions --- src/shared/crypto-util.c | 48 +++++++--------------------------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index 8e6c5a9d44934..5f1041d64c370 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -1866,10 +1866,6 @@ static int ecc_pkey_generate_volume_keys( _cleanup_free_ char *curve_name = NULL; size_t len = 0; - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - if (sym_EVP_PKEY_get_group_name(pkey, NULL, 0, &len) != 1 || len == 0) return log_openssl_errors(LOG_DEBUG, "Failed to determine PKEY group name length"); @@ -2001,16 +1997,10 @@ static int load_key_from_provider( UI_METHOD *ui_method, EVP_PKEY **ret) { - int r; - assert(provider); assert(private_key_uri); assert(ret); - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - /* Load the provider so that this can work without any custom written configuration in /etc/. * Also load the 'default' as that seems to be the recommendation. */ if (!sym_OSSL_PROVIDER_try_load(/* ctx= */ NULL, provider, /* retain_fallbacks= */ true)) @@ -2045,18 +2035,10 @@ static int load_key_from_provider( static int load_key_from_engine(const char *engine, const char *private_key_uri, UI_METHOD *ui_method, EVP_PKEY **ret) { #if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) - int r; -#endif - assert(engine); assert(private_key_uri); assert(ret); -#if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - DISABLE_WARNING_DEPRECATED_DECLARATIONS; _cleanup_(ENGINE_freep) ENGINE *e = sym_ENGINE_by_id(engine); if (!e) @@ -2126,10 +2108,6 @@ static int openssl_load_private_key_from_file(const char *path, EVP_PKEY **ret) assert(path); assert(ret); - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - r = read_full_file_full( AT_FDCWD, path, UINT64_MAX, SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, @@ -2153,17 +2131,9 @@ static int openssl_load_private_key_from_file(const char *path, EVP_PKEY **ret) static int openssl_ask_password_ui_new(const AskPasswordRequest *request, OpenSSLAskPasswordUI **ret) { #ifndef OPENSSL_NO_UI_CONSOLE - int r; -#endif - assert(request); assert(ret); -#ifndef OPENSSL_NO_UI_CONSOLE - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - _cleanup_(UI_destroy_methodp) UI_METHOD *method = sym_UI_create_method("systemd-ask-password"); if (!method) return log_openssl_errors(LOG_DEBUG, "Failed to initialize openssl user interface"); @@ -2202,10 +2172,6 @@ static int load_x509_certificate_from_file(const char *path, X509 **ret) { assert(path); assert(ret); - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - r = read_full_file_full( AT_FDCWD, path, UINT64_MAX, SIZE_MAX, READ_FULL_FILE_CONNECT_SOCKET, @@ -2229,16 +2195,10 @@ static int load_x509_certificate_from_file(const char *path, X509 **ret) { } static int load_x509_certificate_from_provider(const char *provider, const char *certificate_uri, X509 **ret) { - int r; - assert(provider); assert(certificate_uri); assert(ret); - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - /* Load the provider so that this can work without any custom written configuration in /etc/. * Also load the 'default' as that seems to be the recommendation. */ if (!sym_OSSL_PROVIDER_try_load(/* ctx= */ NULL, provider, /* retain_fallbacks= */ true)) @@ -2311,6 +2271,10 @@ int openssl_load_x509_certificate( assert(certificate); + r = dlopen_libcrypto(LOG_DEBUG); + if (r < 0) + return r; + switch (certificate_source_type) { case OPENSSL_CERTIFICATE_SOURCE_FILE: @@ -2350,6 +2314,10 @@ int openssl_load_private_key( assert(ret_private_key); assert(ret_user_interface); + r = dlopen_libcrypto(LOG_DEBUG); + if (r < 0) + return r; + if (private_key_source_type == OPENSSL_KEY_SOURCE_FILE) { r = openssl_load_private_key_from_file(private_key, ret_private_key); if (r < 0) From 7c21061f091d9063f256a9cc4984a8e45d302730 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 1 Jul 2026 14:44:39 +0900 Subject: [PATCH 18/71] crypto-util: make OpenSSL ENGINE API symbols optional during dlopen If systemd is compiled with OpenSSL 3 headers but executed in an environment where OpenSSL 4 (libcrypto.so.4) is loaded, dlopen_many_sym_or_warn() will fail because OpenSSL 4 completely removes the deprecated ENGINE API. This breaks the ability to dynamically fallback and seamlessly upgrade OpenSSL without recompiling systemd. To fix this, drop the ENGINE API symbols from the mandatory DLSYM_ARG() list. Instead, try to load them via DLSYM_OPTIONAL() after the library is opened. load_key_from_engine() is updated to check for their presence and return -EOPNOTSUPP if the loaded OpenSSL version does not provide them. --- src/shared/crypto-util.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index 5f1041d64c370..fddb2ba206bf4 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -631,12 +631,6 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG(i2d_PublicKey), DLSYM_ARG(i2d_X509), DLSYM_ARG(i2d_X509_NAME), -#if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) - DLSYM_ARG_FORCE(ENGINE_by_id), - DLSYM_ARG_FORCE(ENGINE_free), - DLSYM_ARG_FORCE(ENGINE_init), - DLSYM_ARG_FORCE(ENGINE_load_private_key), -#endif #ifndef OPENSSL_NO_UI_CONSOLE DLSYM_ARG(UI_OpenSSL), DLSYM_ARG(UI_create_method), @@ -661,6 +655,15 @@ int dlopen_libcrypto(int log_level) { return -EOPNOTSUPP; /* turn into recognizable error */ } +#if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) + /* Load ENGINE API optionally so we don't fail when loading libcrypto.so.4 even if systemd is built + * with openssl-3 headers. */ + DLSYM_OPTIONAL(libcrypto_dl, ENGINE_by_id); + DLSYM_OPTIONAL(libcrypto_dl, ENGINE_init); + DLSYM_OPTIONAL(libcrypto_dl, ENGINE_free); + DLSYM_OPTIONAL(libcrypto_dl, ENGINE_load_private_key); +#endif + return r; #else return log_full_errno(log_level, SYNTHETIC_ERRNO(EOPNOTSUPP), @@ -2040,6 +2043,13 @@ static int load_key_from_engine(const char *engine, const char *private_key_uri, assert(ret); DISABLE_WARNING_DEPRECATED_DECLARATIONS; + if (!sym_ENGINE_by_id || + !sym_ENGINE_free || + !sym_ENGINE_init || + !sym_ENGINE_load_private_key) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "ENGINE API is not available in the loaded OpenSSL library."); + _cleanup_(ENGINE_freep) ENGINE *e = sym_ENGINE_by_id(engine); if (!e) return log_openssl_errors(LOG_DEBUG, "Failed to load signing engine '%s'", engine); From 7301e2602daed8330a84ac5a38639b143819692a Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 2 Jul 2026 03:29:29 +0900 Subject: [PATCH 19/71] crypto-util: move functions Implementations for loading private/public keys and X.509 certificates were scattered. Group them together to improve readability. --- src/shared/crypto-util.c | 256 +++++++++++++++++++-------------------- 1 file changed, 128 insertions(+), 128 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index fddb2ba206bf4..60e2730bc7e34 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -2070,6 +2070,49 @@ static int load_key_from_engine(const char *engine, const char *private_key_uri, #endif } +static int openssl_load_private_key_from_file(const char *path, EVP_PKEY **ret) { + _cleanup_(erase_and_freep) char *rawkey = NULL; + _cleanup_(BIO_freep) BIO *kb = NULL; + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pk = NULL; + size_t rawkeysz; + int r; + + assert(path); + assert(ret); + + r = read_full_file_full( + AT_FDCWD, path, UINT64_MAX, SIZE_MAX, + READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, + NULL, + &rawkey, &rawkeysz); + if (r < 0) + return log_debug_errno(r, "Failed to read key file '%s': %m", path); + + kb = sym_BIO_new_mem_buf(rawkey, rawkeysz); + if (!kb) + return log_oom_debug(); + + pk = sym_PEM_read_bio_PrivateKey(kb, NULL, NULL, NULL); + if (!pk) + return log_openssl_errors(LOG_DEBUG, "Failed to parse PEM private key"); + + *ret = TAKE_PTR(pk); + + return 0; +} + +OpenSSLAskPasswordUI* openssl_ask_password_ui_free(OpenSSLAskPasswordUI *ui) { + if (!ui) + return NULL; + +#ifndef OPENSSL_NO_UI_CONSOLE + assert(sym_UI_get_default_method() == ui->method); + sym_UI_set_default_method(sym_UI_OpenSSL()); + sym_UI_destroy_method(ui->method); +#endif + return mfree(ui); +} + #ifndef OPENSSL_NO_UI_CONSOLE static int openssl_ask_password_ui_read(UI *ui, UI_STRING *uis) { int r; @@ -2108,37 +2151,6 @@ static int openssl_ask_password_ui_read(UI *ui, UI_STRING *uis) { } #endif -static int openssl_load_private_key_from_file(const char *path, EVP_PKEY **ret) { - _cleanup_(erase_and_freep) char *rawkey = NULL; - _cleanup_(BIO_freep) BIO *kb = NULL; - _cleanup_(EVP_PKEY_freep) EVP_PKEY *pk = NULL; - size_t rawkeysz; - int r; - - assert(path); - assert(ret); - - r = read_full_file_full( - AT_FDCWD, path, UINT64_MAX, SIZE_MAX, - READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, - NULL, - &rawkey, &rawkeysz); - if (r < 0) - return log_debug_errno(r, "Failed to read key file '%s': %m", path); - - kb = sym_BIO_new_mem_buf(rawkey, rawkeysz); - if (!kb) - return log_oom_debug(); - - pk = sym_PEM_read_bio_PrivateKey(kb, NULL, NULL, NULL); - if (!pk) - return log_openssl_errors(LOG_DEBUG, "Failed to parse PEM private key"); - - *ret = TAKE_PTR(pk); - - return 0; -} - static int openssl_ask_password_ui_new(const AskPasswordRequest *request, OpenSSLAskPasswordUI **ret) { #ifndef OPENSSL_NO_UI_CONSOLE assert(request); @@ -2172,6 +2184,91 @@ static int openssl_ask_password_ui_new(const AskPasswordRequest *request, OpenSS #endif } +int openssl_load_private_key( + KeySourceType private_key_source_type, + const char *private_key_source, + const char *private_key, + const AskPasswordRequest *request, + EVP_PKEY **ret_private_key, + OpenSSLAskPasswordUI **ret_user_interface) { + + int r; + + /* The caller must keep the OpenSSLAskPasswordUI object alive as long as the EVP_PKEY object so that + * the user can enter any needed hardware token pin to unlock the private key when needed. */ + + assert(private_key); + assert(request); + assert(ret_private_key); + assert(ret_user_interface); + + r = dlopen_libcrypto(LOG_DEBUG); + if (r < 0) + return r; + + if (private_key_source_type == OPENSSL_KEY_SOURCE_FILE) { + r = openssl_load_private_key_from_file(private_key, ret_private_key); + if (r < 0) + return r; + + *ret_user_interface = NULL; + } else { + _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; + r = openssl_ask_password_ui_new(request, &ui); + if (r < 0) + return log_debug_errno(r, "Failed to allocate ask-password user interface: %m"); + + UI_METHOD *ui_method = NULL; +#ifndef OPENSSL_NO_UI_CONSOLE + ui_method = ui->method; +#endif + + switch (private_key_source_type) { + + case OPENSSL_KEY_SOURCE_ENGINE: + r = load_key_from_engine(private_key_source, private_key, ui_method, ret_private_key); + break; + case OPENSSL_KEY_SOURCE_PROVIDER: + r = load_key_from_provider(private_key_source, private_key, ui_method, ret_private_key); + break; + default: + assert_not_reached(); + } + if (r < 0) + return log_debug_errno( + r, + "Failed to load key '%s' from OpenSSL private key source %s: %m", + private_key, + private_key_source); + + *ret_user_interface = TAKE_PTR(ui); + } + + return 0; +} + +int openssl_extract_public_key(EVP_PKEY *private_key, EVP_PKEY **ret) { + int r; + + assert(private_key); + assert(ret); + + r = dlopen_libcrypto(LOG_DEBUG); + if (r < 0) + return r; + + _cleanup_(OPENSSL_freep) void *buf = NULL; + int len = sym_i2d_PUBKEY(private_key, (unsigned char**) &buf); + if (len < 0) + return log_openssl_errors(LOG_DEBUG, "Failed to extract public key in DER format"); + + const unsigned char *t = buf; + if (!sym_d2i_PUBKEY(ret, &t, len)) + return log_openssl_errors(LOG_DEBUG, "Failed to parse public key in DER format"); + + return 0; +} + static int load_x509_certificate_from_file(const char *path, X509 **ret) { _cleanup_free_ char *rawcert = NULL; _cleanup_(X509_freep) X509 *cert = NULL; @@ -2241,18 +2338,6 @@ static int load_x509_certificate_from_provider(const char *provider, const char return 0; } -OpenSSLAskPasswordUI* openssl_ask_password_ui_free(OpenSSLAskPasswordUI *ui) { - if (!ui) - return NULL; - -#ifndef OPENSSL_NO_UI_CONSOLE - assert(sym_UI_get_default_method() == ui->method); - sym_UI_set_default_method(sym_UI_OpenSSL()); - sym_UI_destroy_method(ui->method); -#endif - return mfree(ui); -} - int x509_fingerprint(X509 *cert, uint8_t buffer[static SHA256_DIGEST_SIZE]) { _cleanup_(OPENSSL_freep) void *der = NULL; int dersz, r; @@ -2305,91 +2390,6 @@ int openssl_load_x509_certificate( return 0; } - -int openssl_load_private_key( - KeySourceType private_key_source_type, - const char *private_key_source, - const char *private_key, - const AskPasswordRequest *request, - EVP_PKEY **ret_private_key, - OpenSSLAskPasswordUI **ret_user_interface) { - - int r; - - /* The caller must keep the OpenSSLAskPasswordUI object alive as long as the EVP_PKEY object so that - * the user can enter any needed hardware token pin to unlock the private key when needed. */ - - assert(private_key); - assert(request); - assert(ret_private_key); - assert(ret_user_interface); - - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - - if (private_key_source_type == OPENSSL_KEY_SOURCE_FILE) { - r = openssl_load_private_key_from_file(private_key, ret_private_key); - if (r < 0) - return r; - - *ret_user_interface = NULL; - } else { - _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; - r = openssl_ask_password_ui_new(request, &ui); - if (r < 0) - return log_debug_errno(r, "Failed to allocate ask-password user interface: %m"); - - UI_METHOD *ui_method = NULL; -#ifndef OPENSSL_NO_UI_CONSOLE - ui_method = ui->method; -#endif - - switch (private_key_source_type) { - - case OPENSSL_KEY_SOURCE_ENGINE: - r = load_key_from_engine(private_key_source, private_key, ui_method, ret_private_key); - break; - case OPENSSL_KEY_SOURCE_PROVIDER: - r = load_key_from_provider(private_key_source, private_key, ui_method, ret_private_key); - break; - default: - assert_not_reached(); - } - if (r < 0) - return log_debug_errno( - r, - "Failed to load key '%s' from OpenSSL private key source %s: %m", - private_key, - private_key_source); - - *ret_user_interface = TAKE_PTR(ui); - } - - return 0; -} - -int openssl_extract_public_key(EVP_PKEY *private_key, EVP_PKEY **ret) { - int r; - - assert(private_key); - assert(ret); - - r = dlopen_libcrypto(LOG_DEBUG); - if (r < 0) - return r; - - _cleanup_(OPENSSL_freep) void *buf = NULL; - int len = sym_i2d_PUBKEY(private_key, (unsigned char**) &buf); - if (len < 0) - return log_openssl_errors(LOG_DEBUG, "Failed to extract public key in DER format"); - - const unsigned char *t = buf; - if (!sym_d2i_PUBKEY(ret, &t, len)) - return log_openssl_errors(LOG_DEBUG, "Failed to parse public key in DER format"); - - return 0; -} #endif int parse_openssl_certificate_source_argument( From f63cc9af18dc10440b79d3b093c5c10b89fb36af Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 2 Jul 2026 03:38:03 +0900 Subject: [PATCH 20/71] crypto-util: drop redundant logs The called functions already log errors internally. --- src/shared/crypto-util.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index 60e2730bc7e34..b3f714acbca22 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -2216,7 +2216,7 @@ int openssl_load_private_key( _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; r = openssl_ask_password_ui_new(request, &ui); if (r < 0) - return log_debug_errno(r, "Failed to allocate ask-password user interface: %m"); + return r; UI_METHOD *ui_method = NULL; #ifndef OPENSSL_NO_UI_CONSOLE @@ -2235,11 +2235,7 @@ int openssl_load_private_key( assert_not_reached(); } if (r < 0) - return log_debug_errno( - r, - "Failed to load key '%s' from OpenSSL private key source %s: %m", - private_key, - private_key_source); + return r; *ret_user_interface = TAKE_PTR(ui); } From 805f8c6503a79138c17031314c1a746784effe3a Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 2 Jul 2026 03:33:04 +0900 Subject: [PATCH 21/71] crypto-util: allow loading private keys from engine/provider without UI support OpenSSL UI is not a mandatory feature to load private keys from an engine or a provider. Let's allow loading private keys even if OpenSSL UI is not supported. Note that even if OPENSSL_NO_UI_CONSOLE is set, the type UI_METHOD is always defined. Hence, the `#ifndef` condition in the definition of struct OpenSSLAskPasswordUI is unnecessary and can be dropped. --- src/shared/crypto-util.c | 57 +++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index b3f714acbca22..f9e0c7f628374 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -31,9 +31,7 @@ struct OpenSSLAskPasswordUI { AskPasswordRequest request; -#ifndef OPENSSL_NO_UI_CONSOLE UI_METHOD *method; -#endif }; DLSYM_PROTOTYPE(ASN1_ANY_it) = NULL; @@ -1997,7 +1995,7 @@ int pkey_generate_volume_keys( static int load_key_from_provider( const char *provider, const char *private_key_uri, - UI_METHOD *ui_method, + UI_METHOD *ui_method, /* can be NULL */ EVP_PKEY **ret) { assert(provider); @@ -2036,7 +2034,12 @@ static int load_key_from_provider( return 0; } -static int load_key_from_engine(const char *engine, const char *private_key_uri, UI_METHOD *ui_method, EVP_PKEY **ret) { +static int load_key_from_engine( + const char *engine, + const char *private_key_uri, + UI_METHOD *ui_method, /* can be NULL */ + EVP_PKEY **ret) { + #if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) assert(engine); assert(private_key_uri); @@ -2152,10 +2155,10 @@ static int openssl_ask_password_ui_read(UI *ui, UI_STRING *uis) { #endif static int openssl_ask_password_ui_new(const AskPasswordRequest *request, OpenSSLAskPasswordUI **ret) { -#ifndef OPENSSL_NO_UI_CONSOLE assert(request); assert(ret); +#ifndef OPENSSL_NO_UI_CONSOLE _cleanup_(UI_destroy_methodp) UI_METHOD *method = sym_UI_create_method("systemd-ask-password"); if (!method) return log_openssl_errors(LOG_DEBUG, "Failed to initialize openssl user interface"); @@ -2180,7 +2183,8 @@ static int openssl_ask_password_ui_new(const AskPasswordRequest *request, OpenSS *ret = TAKE_PTR(ui); return 0; #else - return -EOPNOTSUPP; + *ret = NULL; + return 0; #endif } @@ -2206,40 +2210,33 @@ int openssl_load_private_key( if (r < 0) return r; - if (private_key_source_type == OPENSSL_KEY_SOURCE_FILE) { - r = openssl_load_private_key_from_file(private_key, ret_private_key); - if (r < 0) - return r; + _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; - *ret_user_interface = NULL; - } else { - _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; + switch (private_key_source_type) { + case OPENSSL_KEY_SOURCE_FILE: + r = openssl_load_private_key_from_file(private_key, ret_private_key); + break; + case OPENSSL_KEY_SOURCE_ENGINE: r = openssl_ask_password_ui_new(request, &ui); if (r < 0) return r; - UI_METHOD *ui_method = NULL; -#ifndef OPENSSL_NO_UI_CONSOLE - ui_method = ui->method; -#endif - - switch (private_key_source_type) { - - case OPENSSL_KEY_SOURCE_ENGINE: - r = load_key_from_engine(private_key_source, private_key, ui_method, ret_private_key); - break; - case OPENSSL_KEY_SOURCE_PROVIDER: - r = load_key_from_provider(private_key_source, private_key, ui_method, ret_private_key); - break; - default: - assert_not_reached(); - } + r = load_key_from_engine(private_key_source, private_key, ui ? ui->method : NULL, ret_private_key); + break; + case OPENSSL_KEY_SOURCE_PROVIDER: + r = openssl_ask_password_ui_new(request, &ui); if (r < 0) return r; - *ret_user_interface = TAKE_PTR(ui); + r = load_key_from_provider(private_key_source, private_key, ui ? ui->method : NULL, ret_private_key); + break; + default: + assert_not_reached(); } + if (r < 0) + return r; + *ret_user_interface = TAKE_PTR(ui); return 0; } From d26da0a7057cd0db563c12d2297a57e20528253d Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 2 Jul 2026 03:47:06 +0900 Subject: [PATCH 22/71] crypto-util: make OpenSSL UI API symbols optional during dlopen Previously, if systemd was built with OpenSSL UI support, it would fail to load libcrypto at runtime if the library lacked UI support, requiring a recompilation of systemd to fix. Let's relax this strict requirement by making the UI methods optional during dlopen(). openssl_ui_supported() is added to dynamically check if all required UI symbols were successfully loaded. --- src/shared/crypto-util.c | 72 +++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index f9e0c7f628374..a6aa020bdce14 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -628,23 +628,7 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG(i2d_PUBKEY), DLSYM_ARG(i2d_PublicKey), DLSYM_ARG(i2d_X509), - DLSYM_ARG(i2d_X509_NAME), -#ifndef OPENSSL_NO_UI_CONSOLE - DLSYM_ARG(UI_OpenSSL), - DLSYM_ARG(UI_create_method), - DLSYM_ARG(UI_destroy_method), - DLSYM_ARG(UI_get0_output_string), - DLSYM_ARG(UI_get_default_method), - DLSYM_ARG(UI_get_method), - DLSYM_ARG(UI_get_string_type), - DLSYM_ARG(UI_method_get_ex_data), - DLSYM_ARG(UI_method_get_reader), - DLSYM_ARG(UI_method_set_ex_data), - DLSYM_ARG(UI_method_set_reader), - DLSYM_ARG(UI_set_default_method), - DLSYM_ARG(UI_set_result), -#endif - NULL); + DLSYM_ARG(i2d_X509_NAME)); if (r >= 0) break; } @@ -653,6 +637,24 @@ int dlopen_libcrypto(int log_level) { return -EOPNOTSUPP; /* turn into recognizable error */ } +#ifndef OPENSSL_NO_UI_CONSOLE + /* Load UI API optionally so we don't fail to load libcrypto.so lacking UI support, + * even if systemd is built with UI support enabled in the headers. */ + DLSYM_OPTIONAL(libcrypto_dl, UI_OpenSSL); + DLSYM_OPTIONAL(libcrypto_dl, UI_create_method); + DLSYM_OPTIONAL(libcrypto_dl, UI_destroy_method); + DLSYM_OPTIONAL(libcrypto_dl, UI_get0_output_string); + DLSYM_OPTIONAL(libcrypto_dl, UI_get_default_method); + DLSYM_OPTIONAL(libcrypto_dl, UI_get_method); + DLSYM_OPTIONAL(libcrypto_dl, UI_get_string_type); + DLSYM_OPTIONAL(libcrypto_dl, UI_method_get_ex_data); + DLSYM_OPTIONAL(libcrypto_dl, UI_method_get_reader); + DLSYM_OPTIONAL(libcrypto_dl, UI_method_set_ex_data); + DLSYM_OPTIONAL(libcrypto_dl, UI_method_set_reader); + DLSYM_OPTIONAL(libcrypto_dl, UI_set_default_method); + DLSYM_OPTIONAL(libcrypto_dl, UI_set_result); +#endif + #if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0) /* Load ENGINE API optionally so we don't fail when loading libcrypto.so.4 even if systemd is built * with openssl-3 headers. */ @@ -2104,10 +2106,33 @@ static int openssl_load_private_key_from_file(const char *path, EVP_PKEY **ret) return 0; } +static bool openssl_ui_supported(void) { +#ifndef OPENSSL_NO_UI_CONSOLE + return + sym_UI_OpenSSL && + sym_UI_create_method && + sym_UI_destroy_method && + sym_UI_get0_output_string && + sym_UI_get_default_method && + sym_UI_get_method && + sym_UI_get_string_type && + sym_UI_method_get_ex_data && + sym_UI_method_get_reader && + sym_UI_method_set_ex_data && + sym_UI_method_set_reader && + sym_UI_set_default_method && + sym_UI_set_result; +#else + return false; +#endif +} + OpenSSLAskPasswordUI* openssl_ask_password_ui_free(OpenSSLAskPasswordUI *ui) { if (!ui) return NULL; + assert(openssl_ui_supported()); + #ifndef OPENSSL_NO_UI_CONSOLE assert(sym_UI_get_default_method() == ui->method); sym_UI_set_default_method(sym_UI_OpenSSL()); @@ -2120,7 +2145,9 @@ OpenSSLAskPasswordUI* openssl_ask_password_ui_free(OpenSSLAskPasswordUI *ui) { static int openssl_ask_password_ui_read(UI *ui, UI_STRING *uis) { int r; - switch(sym_UI_get_string_type(uis)) { + assert(openssl_ui_supported()); + + switch (sym_UI_get_string_type(uis)) { case UIT_PROMPT: { /* If no ask password request was configured use the default openssl UI. */ AskPasswordRequest *req = (AskPasswordRequest*) sym_UI_method_get_ex_data(sym_UI_get_method(ui), 0); @@ -2158,6 +2185,12 @@ static int openssl_ask_password_ui_new(const AskPasswordRequest *request, OpenSS assert(request); assert(ret); + if (!openssl_ui_supported()) { + log_debug("OpenSSL UI API is not supported."); + *ret = NULL; + return 0; + } + #ifndef OPENSSL_NO_UI_CONSOLE _cleanup_(UI_destroy_methodp) UI_METHOD *method = sym_UI_create_method("systemd-ask-password"); if (!method) @@ -2183,8 +2216,7 @@ static int openssl_ask_password_ui_new(const AskPasswordRequest *request, OpenSS *ret = TAKE_PTR(ui); return 0; #else - *ret = NULL; - return 0; + assert_not_reached(); #endif } From 71a94be08c54a8f50564f546fd31cf9965e9333f Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 2 Jul 2026 04:46:51 +0900 Subject: [PATCH 23/71] crypto-util: drop unused symbol --- src/shared/crypto-util.c | 2 -- src/shared/crypto-util.h | 1 - 2 files changed, 3 deletions(-) diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c index a6aa020bdce14..d791996efc7a2 100644 --- a/src/shared/crypto-util.c +++ b/src/shared/crypto-util.c @@ -274,7 +274,6 @@ DLSYM_PROTOTYPE(PKCS7_set_content) = NULL; static DLSYM_PROTOTYPE(PKCS7_set_type) = NULL; DLSYM_PROTOTYPE(PKCS7_sign) = NULL; DLSYM_PROTOTYPE(PKCS7_verify) = NULL; -DLSYM_PROTOTYPE(SHA1) = NULL; DLSYM_PROTOTYPE(SHA512) = NULL; DLSYM_PROTOTYPE(X509_ALGOR_free) = NULL; static DLSYM_PROTOTYPE(X509_ALGOR_set0) = NULL; @@ -596,7 +595,6 @@ int dlopen_libcrypto(int log_level) { DLSYM_ARG(PKCS7_set_type), DLSYM_ARG(PKCS7_sign), DLSYM_ARG(PKCS7_verify), - DLSYM_ARG(SHA1), DLSYM_ARG(SHA512), DLSYM_ARG(X509_ALGOR_free), DLSYM_ARG(X509_ALGOR_set0), diff --git a/src/shared/crypto-util.h b/src/shared/crypto-util.h index b0358090a7797..42a3f2f8c0e61 100644 --- a/src/shared/crypto-util.h +++ b/src/shared/crypto-util.h @@ -255,7 +255,6 @@ extern DLSYM_PROTOTYPE(PKCS7_new); extern DLSYM_PROTOTYPE(PKCS7_set_content); extern DLSYM_PROTOTYPE(PKCS7_sign); extern DLSYM_PROTOTYPE(PKCS7_verify); -extern DLSYM_PROTOTYPE(SHA1); extern DLSYM_PROTOTYPE(SHA512); extern DLSYM_PROTOTYPE(X509_ALGOR_free); extern DLSYM_PROTOTYPE(X509_ATTRIBUTE_free); From 4bf6e751984db9bf35a516a4177bb5b3493989dc Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 2 Jul 2026 04:47:32 +0900 Subject: [PATCH 24/71] ci/build-test: try to build with OPENSSL_NO_DEPRECATED --- .github/workflows/build-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.sh b/.github/workflows/build-test.sh index 9c3d2e73382e2..084e2c4f1af16 100755 --- a/.github/workflows/build-test.sh +++ b/.github/workflows/build-test.sh @@ -12,7 +12,7 @@ success() { echo >&2 -e "\033[32;1m$1\033[0m"; } ARGS=( "--optimization=0 -Dopenssl=disabled -Dtpm=true -Dtpm2=enabled" "--optimization=s -Dutmp=false -Dc_args='-DOPENSSL_NO_UI_CONSOLE=1'" - "--optimization=2 -Ddns-over-tls=openssl" + "--optimization=2 -Ddns-over-tls=openssl -Dc_args='-DOPENSSL_NO_DEPRECATED'" "--optimization=3 -Db_lto=true -Ddns-over-tls=false" "--optimization=3 -Db_lto=false -Dtpm2=disabled -Dlibfido2=disabled -Dp11kit=disabled -Defi=false -Dbootloader=disabled" "--optimization=3 -Dfexecve=true -Dstandalone-binaries=true -Dstatic-libsystemd=true -Dstatic-libudev=true" From d04423c870a7b62f376e26ce7a4bc48d2370f423 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 2 Jul 2026 17:31:38 +0900 Subject: [PATCH 25/71] bless-boot: avoid false maybe-uninitialized warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obserbed with GCC-11 on Ubuntu. ``` In file included from ../src/shared/format-table.h:7, from ../src/bless-boot/bless-boot.c:11: ../src/bless-boot/bless-boot.c: In function ‘verb_set’: ../src/basic/log.h:187:27: error: ‘source2’ may be used uninitialized in this function [-Werror=maybe-uninitialized] 187 | ? log_internal(_level, _e, PROJECT_FILE, __LINE__, __func__, __VA_ARGS__) \ | ^~~~~~~~~~~~ ../src/bless-boot/bless-boot.c:458:40: note: ‘source2’ was declared here 458 | const char *target, *source1, *source2; | ^~~~~~~ In file included from ../src/shared/format-table.h:7, from ../src/bless-boot/bless-boot.c:11: ../src/basic/log.h:187:27: error: ‘source1’ may be used uninitialized in this function [-Werror=maybe-uninitialized] 187 | ? log_internal(_level, _e, PROJECT_FILE, __LINE__, __func__, __VA_ARGS__) \ | ^~~~~~~~~~~~ ../src/bless-boot/bless-boot.c:458:30: note: ‘source1’ was declared here 458 | const char *target, *source1, *source2; | ^~~~~~~ In file included from ../src/shared/format-table.h:7, from ../src/bless-boot/bless-boot.c:11: ../src/basic/log.h:187:27: error: ‘target’ may be used uninitialized in this function [-Werror=maybe-uninitialized] 187 | ? log_internal(_level, _e, PROJECT_FILE, __LINE__, __func__, __VA_ARGS__) \ | ^~~~~~~~~~~~ ../src/bless-boot/bless-boot.c:458:21: note: ‘target’ was declared here 458 | const char *target, *source1, *source2; | ^~~~~~ cc1: all warnings being treated as errors ``` --- src/bless-boot/bless-boot.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bless-boot/bless-boot.c b/src/bless-boot/bless-boot.c index 43fb72cddb72b..4242182ac10b4 100644 --- a/src/bless-boot/bless-boot.c +++ b/src/bless-boot/bless-boot.c @@ -455,7 +455,7 @@ VERB_FULL(verb_set, "indeterminate", NULL, VERB_ANY, 1, 0, STATUS_INDETERMINATE, "Undo any marking as good or bad"); static int verb_set(int argc, char *argv[], uintptr_t data, void *userdata) { _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL; - const char *target, *source1, *source2; + const char *target = NULL, *source1 = NULL, *source2 = NULL; /* avoid false maybe-uninitialized warning */ uint64_t left, done; Status status = data; int r; From 55be5e189086a147a97cc25b8f58c0b89f6c3d05 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 2 Jul 2026 08:52:16 +0200 Subject: [PATCH 26/71] tpm2: optionally disable TPMA_NV_ORDERLY for NvPCRs NVIndexes in TPMs can operate in two modes: 1. Backed by TPM RAM. In this case they are only written to NVRAM on an orderly TPM shutdown when the system goes down. (TPMA_NV_ORDERLY flag is on) 2. Backed by TPM NVRAM. In this case the nvindex value is written to NVRAM on every write, and things are not delayed until orderly shutdown. Normally mode 1 sounds like the obvious choice for NvPCRs, which reset to zero anyway at boot. However, things are more complicated since real-life TPMs tend to have a lot less RAM than NVRAM (both are constrained but RAM even more than NVRAM). Hence there's value in using NVRAM right-away. However, writing to NVRAM all the time means wearing it out (since NVRAM is more vulnerable to that). So far we unconditionally went for mode 1, but ran into space constraints of RAM due to that. Let's improve things a bit, and use orderly mode for NvPCRs we expect to write many times, and non-orderly mode for those we expect to write only a small, fixed number of times at boot, and not anymore during runtime. Right now, this is only the "hardware" NvPCR, which measures hw identity at boot. Hopefully, this stretches available resources a bit further. This also makes sure if the flag was set differently on allocation as we'd set now, we accept it and won't complain, to make upgrades safe. Suggested by Andreas Fuchs. --- docs/TPM2_PCR_MEASUREMENTS.md | 12 ++++++++++++ src/shared/tpm2-util.c | 9 +++++++-- src/tpm2-setup/nvpcr/hardware.nvpcr.in | 3 ++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/TPM2_PCR_MEASUREMENTS.md b/docs/TPM2_PCR_MEASUREMENTS.md index faa918f6319f6..87d8d3b675734 100644 --- a/docs/TPM2_PCR_MEASUREMENTS.md +++ b/docs/TPM2_PCR_MEASUREMENTS.md @@ -74,6 +74,18 @@ fields are: least important ones are skipped gracefully rather than the allocation failing arbitrarily. Ties are broken by name. Priority does not affect the NV index, the algorithm, or anything measured into the NvPCR. +* `orderly` — a boolean, defaulting to `true`. It controls whether the NV index + is allocated with the `TPMA_NV_ORDERLY` attribute set, which selects whether + the TPM keeps the NV index in RAM or in persistent memory (NVRAM). On + physical TPMs RAM is typically much more constrained than persistent memory, + but persistent memory is subject to wear. We hence prefer `TPMA_NV_ORDERLY` + disabled (i.e. NVRAM) for NvPCRs that are written only once each boot — which + translates into a conservative number of write cycles over the lifetime of a + TPM — but enabled (i.e. RAM) for NvPCRs we expect to be written many times + during runtime, so that we minimize wear. This reflects real-life experience + where the RAM in TPMs is so constrained that allocating many NvPCRs in TPM + RAM simply doesn't work. For now, only the `hardware` NvPCR (which is written + just once, early at boot) sets this flag to false. There's one complication: these NV indexes (like any NV indexes) can be deleted by anyone with access to the TPM, and then be recreated. This could be used to diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 3a9fbaa7e47e3..1920d6f171927 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -7183,6 +7183,7 @@ static int tpm2_define_nvpcr_nv_index( const Tpm2Handle *session, TPM2_HANDLE nv_index, TPMI_ALG_HASH algorithm, + bool orderly, Tpm2Handle **ret_nv_handle) { _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; @@ -7215,7 +7216,7 @@ static int tpm2_define_nvpcr_nv_index( .nvIndex = nv_index, .nameAlg = algorithm, .attributes = TPMA_NV_CLEAR_STCLEAR | - TPMA_NV_ORDERLY | + (orderly ? TPMA_NV_ORDERLY : 0) | TPMA_NV_OWNERWRITE | TPMA_NV_AUTHWRITE | TPMA_NV_OWNERREAD | @@ -7259,7 +7260,7 @@ static int tpm2_define_nvpcr_nv_index( if (nv_public_real->size < endoffsetof_field(TPMS_NV_PUBLIC, attributes) + sizeof_field(TPMS_NV_PUBLIC, dataSize) || nv_public_real->nvPublic.nvIndex != public_info.nvPublic.nvIndex || nv_public_real->nvPublic.nameAlg != public_info.nvPublic.nameAlg || - ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~TPMA_NV_WRITTEN) != 0 || + ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~(TPMA_NV_WRITTEN|TPMA_NV_ORDERLY)) != 0 || nv_public_real->nvPublic.dataSize != public_info.nvPublic.dataSize) return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), "Public data of nvindex 0x%x does not match our expectations.", nv_index); @@ -7976,6 +7977,7 @@ typedef struct NvPCRData { uint16_t algorithm; uint32_t nv_index; uint64_t priority; + bool orderly; } NvPCRData; static void nvpcr_data_done(NvPCRData *d) { @@ -8016,12 +8018,14 @@ static int nvpcr_data_load(const char *name, NvPCRData *ret) { { "algorithm", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_tpm2_algorithm, offsetof(NvPCRData, algorithm), 0 }, { "nvIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(NvPCRData, nv_index), SD_JSON_MANDATORY }, { "priority", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(NvPCRData, priority), 0 }, + { "orderly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(NvPCRData, orderly), 0 }, {}, }; _cleanup_(nvpcr_data_done) NvPCRData p = { .algorithm = TPM2_ALG_SHA256, .priority = TPM2_NVPCR_PRIORITY_DEFAULT, + .orderly = true, }; r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) @@ -8652,6 +8656,7 @@ int tpm2_nvpcr_initialize( session, p.nv_index, p.algorithm, + p.orderly, &nv_handle); if (r < 0) return r; diff --git a/src/tpm2-setup/nvpcr/hardware.nvpcr.in b/src/tpm2-setup/nvpcr/hardware.nvpcr.in index bb4a3afa6957a..0fdf32d309459 100644 --- a/src/tpm2-setup/nvpcr/hardware.nvpcr.in +++ b/src/tpm2-setup/nvpcr/hardware.nvpcr.in @@ -2,5 +2,6 @@ "name" : "hardware", "algorithm" : "sha256", "nvIndex" : {{TPM2_NVPCR_BASE + 0}}, - "priority" : 500 + "priority" : 500, + "orderly" : false } From 8aaa8dec59a9d7dca37ed03ef6550a23c6268ec0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 2 Jul 2026 09:13:35 +0200 Subject: [PATCH 27/71] tpm2: cache NvPCR NV space exhaustion via flag files in /run/ When we run out of NV index space while allocating an NvPCR, the situation will unlikely improve until (at least) reboot. Retrying the (doomed) Esys_NV_DefineSpace call on every subsequent allocation attempt is wasteful (and very slow), so remember the exhaustion in a flag file under /run/ and fail early next time. We use two separate flag files, one for orderly and one for non-orderly NvPCRs, since the two draw on different TPM resources (RAM-backed vs. NVRAM-backed): exhaustion of one doesn't imply exhaustion of the other. The files live in /run/, hence are cleared on reboot, which is potentially is when NV space might become available again. --- src/shared/tpm2-util.c | 138 +++++++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 53 deletions(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 1920d6f171927..27606ab73d04c 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -7186,7 +7186,6 @@ static int tpm2_define_nvpcr_nv_index( bool orderly, Tpm2Handle **ret_nv_handle) { - _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; TSS2_RC rc; int r; @@ -7204,11 +7203,13 @@ static int tpm2_define_nvpcr_nv_index( if ((size_t) digest_size > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer)) return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Digest too large for extension."); - r = tpm2_handle_new(c, &new_handle); - if (r < 0) - return r; - - new_handle->flush = false; /* This is a persistent NV index, don't flush hence */ + /* If we already ran into NV index space exhaustion for this orderly mode during this boot, don't + * bother trying again — the situation is unlikely to improve until reboot. We track this via a flag + * file in /run/, with a separate file for orderly and non-orderly NvPCRs, since the two draw on + * different TPM resources (RAM-backed vs. NVRAM-backed). */ + const char *exhausted_flag = orderly ? + "/run/systemd/tpm2-nv-space-exhausted-orderly" : + "/run/systemd/tpm2-nv-space-exhausted-non-orderly"; TPM2B_NV_PUBLIC public_info = { .size = sizeof_field(TPM2B_NV_PUBLIC, nvPublic), @@ -7226,62 +7227,93 @@ static int tpm2_define_nvpcr_nv_index( }, }; - rc = sym_Esys_NV_DefineSpace( - c->esys_context, - /* authHandle= */ ESYS_TR_RH_OWNER, - /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, - /* shandle2= */ ESYS_TR_NONE, - /* shandle3= */ ESYS_TR_NONE, - /* auth= */ NULL, - &public_info, - &new_handle->esys_handle); - if (rc == TPM2_RC_NV_SPACE) - return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS), - "NV index space on TPM exhausted, cannot allocate NvPCR."); - if (rc == TPM2_RC_NV_DEFINED) { - log_debug("NV index 0x%" PRIu32 " already registered.", nv_index); - - new_handle = tpm2_handle_free(new_handle); - - _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public_real = NULL; - r = tpm2_nv_index_to_handle( - c, - nv_index, - session, - &nv_public_real, - /* ret_name= */ NULL, - &new_handle); - if (r <= 0) - return log_debug_errno(r < 0 ? r : SYNTHETIC_ERRNO(ENOTRECOVERABLE), - "Failed to acquire handle to existing NV index 0x%" PRIu32 ".", nv_index); - - log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", nv_index); - - if (nv_public_real->size < endoffsetof_field(TPMS_NV_PUBLIC, attributes) + sizeof_field(TPMS_NV_PUBLIC, dataSize) || - nv_public_real->nvPublic.nvIndex != public_info.nvPublic.nvIndex || - nv_public_real->nvPublic.nameAlg != public_info.nvPublic.nameAlg || - ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~(TPMA_NV_WRITTEN|TPMA_NV_ORDERLY)) != 0 || - nv_public_real->nvPublic.dataSize != public_info.nvPublic.dataSize) - return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), - "Public data of nvindex 0x%x does not match our expectations.", nv_index); + bool exhausted; + if (access(exhausted_flag, F_OK) < 0) { + if (errno != ENOENT) + log_debug_errno(errno, "Failed to check whether %s exists, assuming it does not: %m", exhausted_flag); - log_debug("Public info for nvindex 0x%x checks out, using.", nv_index); + _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; + r = tpm2_handle_new(c, &new_handle); + if (r < 0) + return r; - if (ret_nv_handle) - *ret_nv_handle = TAKE_PTR(new_handle); + new_handle->flush = false; /* This is a persistent NV index, don't flush hence */ - return 0; + rc = sym_Esys_NV_DefineSpace( + c->esys_context, + /* authHandle= */ ESYS_TR_RH_OWNER, + /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + /* auth= */ NULL, + &public_info, + &new_handle->esys_handle); + if (rc == TPM2_RC_NV_SPACE) { + /* Remember that we ran out of NV index space for this orderly mode, so that we don't keep + * retrying the (doomed) allocation until reboot. */ + r = touch(exhausted_flag); + if (r < 0) + log_debug_errno(r, "Failed to create %s flag file, ignoring: %m", exhausted_flag); + + return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS), + "NV index space on TPM exhausted, cannot allocate NvPCR."); + } + if (rc == TSS2_RC_SUCCESS) { + log_debug("NV Index 0x%" PRIx32 " successfully allocated.", nv_index); + + if (ret_nv_handle) + *ret_nv_handle = TAKE_PTR(new_handle); + + return 1; + } + if (rc != TPM2_RC_NV_DEFINED) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc)); + + log_debug("NV index 0x%" PRIx32 " already registered.", nv_index); + exhausted = false; + } else { + log_debug("TPM NV index space previously found exhausted (%s exists), refusing to allocate %s NvPCR, but checking if it already exists.", + exhausted_flag, orderly ? "orderly" : "non-orderly"); + exhausted = true; } - if (rc != TSS2_RC_SUCCESS) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), - "Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc)); - log_debug("NV Index 0x%" PRIx32 " successfully allocated.", nv_index); + /* We either got told that this NV index already exists or we didn't even try to allocate it, because + * it failed before. Let's get information about it, in the hope it exists. */ + + _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public_real = NULL; + _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; + r = tpm2_nv_index_to_handle( + c, + nv_index, + session, + &nv_public_real, + /* ret_name= */ NULL, + &new_handle); + if (r <= 0) { + if (exhausted) + return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS), "Unable to acquire NvPCR and space exhaustion was indicated before."); + + return log_debug_errno(r < 0 ? r : SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to acquire handle to NV index 0x%" PRIx32 ".", nv_index); + } + + log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", nv_index); + + if (nv_public_real->size < endoffsetof_field(TPMS_NV_PUBLIC, attributes) + sizeof_field(TPMS_NV_PUBLIC, dataSize) || + nv_public_real->nvPublic.nvIndex != public_info.nvPublic.nvIndex || + nv_public_real->nvPublic.nameAlg != public_info.nvPublic.nameAlg || + ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~(TPMA_NV_WRITTEN|TPMA_NV_ORDERLY)) != 0 || + nv_public_real->nvPublic.dataSize != public_info.nvPublic.dataSize) + return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), + "Public data of nvindex 0x%" PRIx32 " does not match our expectations.", nv_index); + + log_debug("Public info for nvindex 0x%" PRIx32 " checks out, using.", nv_index); if (ret_nv_handle) *ret_nv_handle = TAKE_PTR(new_handle); - return 1; + return 0; } static int tpm2_extend_nvpcr_nv_index( From a84642ed94cf25784449919468e110c08229d75d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 2 Jul 2026 09:56:05 +0200 Subject: [PATCH 28/71] update TODO --- TODO.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/TODO.md b/TODO.md index 8ef085f693e40..f7b9585f6d43c 100644 --- a/TODO.md +++ b/TODO.md @@ -938,16 +938,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - drop nss-myhostname in favour of nss-resolve? -- drop NV_ORDERLY flag from the product uuid nvpcr. Effect of the flag is that - it pushes the thing into TPM RAM, but a TPM usually has very little of that, - less than NVRAM. hence setting the flag amplifies space issues. Unsetting the - flag increases wear issues on the NVRAM, however, but this should be limited - for the product uuid nvpcr, since its only changed once per boot. this needs - to be configurable by nvpcr however, as other nvpcrs are different, - i.e. verity one receives many writes during system uptime quite - possibly. (also, NV_ORDERLY makes stuff faster, and dropping it costs - possibly up to 100ms supposedly) - - **EFI:** - honor timezone efi variables for default timezone selection (if there are any?) From 7273d383355fd15c17dc21afe945d949789c87f7 Mon Sep 17 00:00:00 2001 From: Frantisek Sumsal Date: Thu, 2 Jul 2026 14:32:36 +0200 Subject: [PATCH 29/71] test: ignore fails when the formatted timezone differs from the current one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When formatting a timestamp the C API takes into account historical data from tzdata, so it returns a date strings with a historically-correct timezone abbreviation. However, tzname[] doesn't do this and it returns the most recent abbreviation for the given zone. For example, according to tzdata America/Cancun switched from EST/EDT to CST/CDT on 1998-08-02: Zone America/Cancun -5:47:04 - LMT 1922 Jan 1 6:00u -6:00 - CST 1981 Dec 26 2:00 -5:00 - EST 1983 Jan 4 0:00 -6:00 Mexico C%sT 1997 Oct 26 2:00 -5:00 Mexico E%sT 1998 Aug 2 2:00 -6:00 Mexico C%sT 2015 Feb 1 2:00 -5:00 - EST So, formatting a timestamp from this time will yield a string with the EDT timezone: $ TZ=America/Cancun date -d "@902035565" Sun Aug 2 01:26:05 EDT 1998 But using tzname[] (or strptime %z) shows the most recent data, where America/Cancun uses EST (and doesn't use DST anymore, hence tzname[1]=CDT that glibc remembers from the previous zone epoch): $ TZ=America/Cancun ./tz {EST, CDT} This means that when we parse the formatted timestamp back we don't use the historical timezone data, so we might end up with a different offset: TZ=America/Cancun, tzname[0]=EST, tzname[1]=CDT @902035565603993 → Sun 1998-08-02 01:26:05 EDT → @902039165000000 → Sun 1998-08-02 01:26:05 CDT src/test/test-time-util.c:452: Assertion failed: Expected "ignore" to be true Aborted (core dumped) build-local/test-time-util Instead of adding exceptions for every single timezone that switched between different offsets in the past, let's address this a bit more generally and skip the check if the parsed timezone doesn't match any of the current timezones - this still keeps the check that the time difference in such case is exactly one hour, so its effect should be limited mostly to DST-related changes. Resolves: #37684 --- src/test/test-time-util.c | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/test/test-time-util.c b/src/test/test-time-util.c index 392b06214b0ff..1a1370eda6da7 100644 --- a/src/test/test-time-util.c +++ b/src/test/test-time-util.c @@ -433,15 +433,27 @@ static void test_format_timestamp_impl(usec_t x) { if (x_sec == y_sec && streq(xx, yy)) return; /* Yay! */ - /* When the timezone is built with rearguard being enabled (e.g. old Ubuntu and RHEL), the timezone - * Africa/Windhoek may provide time shifted 1 hour from the original. See - * https://github.com/systemd/systemd/issues/28472 and https://github.com/systemd/systemd/pull/35471. - * Also, the same may happen on MSK timezone (e.g. Europe/Volgograd or Europe/Kirov), or on - * Africa/Tripoli (Libya) which switched between CET and EET multiple times historically, causing - * certain timestamps to round-trip with a 1h offset. */ + /* There are two classes of known round-trip failures that produce exactly a 1h offset: + * + * 1) The formatted abbreviation doesn't match the current timezone's abbreviations - the timestamp + * is from a historical era (e.g. Africa/Tripoli switched between CET and EET multiple times + * historically, America/Cancun switched between EST/EDT and CST/CDT several times in the past, + * etc.), and the round-trip is inherently unreliable on platforms where parse_gmtoff() resolves + * such abbreviations with incorrect offsets. + * + * 2) Rearguard/vanguard database format differences where the abbreviation matches but the + * offset is still wrong (e.g. Africa/Windhoek, Europe/Kirov, Europe/Volgograd). + * + * See: + * - https://github.com/systemd/systemd/issues/28472 + * - https://github.com/systemd/systemd/pull/35471 + * - https://github.com/systemd/systemd/issues/37684 + */ bool ignore = - (STRPTR_IN_SET(getenv("TZ"), "Africa/Windhoek", "Africa/Tripoli", "Libya") || - STRPTR_IN_SET(get_tzname(/* dst= */ false), "CAT", "EAT", "MSK", "WET")) && + ((!streq_ptr(tz, get_tzname(/* dst= */ false)) && + !streq_ptr(tz, get_tzname(/* dst= */ true))) || + streq_ptr(getenv("TZ"), "Africa/Windhoek") || + STRPTR_IN_SET(get_tzname(/* dst= */ false), "MSK", "WET")) && (x_sec > y_sec ? x_sec - y_sec : y_sec - x_sec) == 3600; log_full(ignore ? LOG_WARNING : LOG_ERR, @@ -459,16 +471,17 @@ static void test_format_timestamp_loop(void) { test_format_timestamp_impl(USEC_TIMESTAMP_FORMATTABLE_MAX-1); test_format_timestamp_impl(USEC_TIMESTAMP_FORMATTABLE_MAX); - /* Two cases which trigger https://github.com/systemd/systemd/issues/28472 */ + /* Specific timestamps known to cause a 1h round-trip discrepancy with certain timezones: + * + * Two cases which trigger https://github.com/systemd/systemd/issues/28472. */ test_format_timestamp_impl(1504938962980066); test_format_timestamp_impl(1509482094632752); - /* With tzdata-2025c, the timestamp (randomly?) fails on MSK time zone (e.g. Europe/Volgograd). */ test_format_timestamp_impl(1414277092997572); - - /* Africa/Tripoli (Libya) switched from CET to EET multiple times in the past, causing a 1h - * round-trip discrepancy for historical timestamps. */ + /* Africa/Tripoli (Libya) switched from CET to EET multiple times in the past. */ test_format_timestamp_impl(378687574661411); + /* America/Cancun switched from EST/EDT to CST/CDT multiple times in the past. */ + test_format_timestamp_impl(902035565603993); for (unsigned i = 0; i < TRIAL; i++) { usec_t x; From 21de611f695b25d9fac59c96455fec9690b87b59 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Wed, 1 Jul 2026 14:57:33 +0800 Subject: [PATCH 30/71] calendarspec: warn on weekday/date conflict in systemd-analyze and systemd-run When a fixed date (e.g. 2027-01-01) is paired with a weekday constraint (e.g. Thu) that does not match, the timer silently never elapses. Add calendar_spec_from_string_full(..., warn_on_weekday_mismatch) so user-facing tools can opt in to a log_warning() at parse time: - systemd-analyze calendar: uses _full(true) - systemd-run --on-calendar: uses _full(true) - .timer OnCalendar=: uses log_syntax() with file/line context Add test_calendar_spec_weekday_conflict(): forks a child with stderr captured in a memfd via pidref_safe_fork_full(), verifies the warning is emitted for conflicting specs and suppressed for valid ones. Fixes: #40350 Signed-off-by: dongshengyuan --- src/analyze/analyze-calendar.c | 2 +- src/basic/time-util.c | 33 +++++++------ src/basic/time-util.h | 16 +++++++ src/core/load-fragment.c | 8 ++++ src/run/run.c | 21 +++++---- src/shared/calendarspec.c | 85 +++++++++++++++++++++++++++------- src/shared/calendarspec.h | 7 ++- src/test/test-calendarspec.c | 75 ++++++++++++++++++++++++++++++ 8 files changed, 205 insertions(+), 42 deletions(-) diff --git a/src/analyze/analyze-calendar.c b/src/analyze/analyze-calendar.c index a9fb1e897e67e..f6d434de36f58 100644 --- a/src/analyze/analyze-calendar.c +++ b/src/analyze/analyze-calendar.c @@ -19,7 +19,7 @@ static int test_calendar_one(usec_t n, const char *p) { TableCell *cell; int r; - r = calendar_spec_from_string(p, &spec); + r = calendar_spec_from_string_full(p, &spec, /* warn_on_weekday_mismatch= */ true); if (r < 0) { log_error_errno(r, "Failed to parse calendar specification '%s': %m", p); time_parsing_hint(p, /* calendar= */ false, /* timestamp= */ true, /* timespan= */ true); diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 93173c4d917f3..44c5306741174 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -321,24 +321,26 @@ struct timeval *timeval_store(struct timeval *tv, usec_t u) { return tv; } +/* Returns the abbreviated English weekday name for wd in Mon=0 … Sun=6 order. + * We use non-localized (English) form so that timestamps can be parsed with + * parse_timestamp() and always read the same regardless of locale. */ +static const char *const weekday_table[_WEEKDAY_MAX] = { + [WEEKDAY_MON] = "Mon", + [WEEKDAY_TUE] = "Tue", + [WEEKDAY_WED] = "Wed", + [WEEKDAY_THU] = "Thu", + [WEEKDAY_FRI] = "Fri", + [WEEKDAY_SAT] = "Sat", + [WEEKDAY_SUN] = "Sun", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(weekday, int); + char* format_timestamp_style( char *buf, size_t l, usec_t t, TimestampStyle style) { - - /* The weekdays in non-localized (English) form. We use this instead of the localized form, so that - * our generated timestamps may be parsed with parse_timestamp(), and always read the same. */ - static const char * const weekdays[] = { - [0] = "Sun", - [1] = "Mon", - [2] = "Tue", - [3] = "Wed", - [4] = "Thu", - [5] = "Fri", - [6] = "Sat", - }; - struct tm tm; bool utc, us; size_t n; @@ -387,8 +389,9 @@ char* format_timestamp_style( return NULL; /* Start with the week day */ - assert((size_t) tm.tm_wday < ELEMENTSOF(weekdays)); - memcpy(buf, weekdays[tm.tm_wday], 4); + const char *weekday = weekday_to_string(tm.tm_wday == 0 ? 6 : tm.tm_wday - 1); + assert(weekday); + memcpy(buf, weekday, 4); if (style == TIMESTAMP_DATE) { /* Special format string if only date should be shown. */ diff --git a/src/basic/time-util.h b/src/basic/time-util.h index fb0aab3ff5c38..12004497289d7 100644 --- a/src/basic/time-util.h +++ b/src/basic/time-util.h @@ -56,6 +56,18 @@ typedef enum TimestampStyle { #define USEC_PER_YEAR ((usec_t) (31557600ULL*USEC_PER_SEC)) #define NSEC_PER_YEAR ((nsec_t) (31557600ULL*NSEC_PER_SEC)) +enum { + WEEKDAY_MON, + WEEKDAY_TUE, + WEEKDAY_WED, + WEEKDAY_THU, + WEEKDAY_FRI, + WEEKDAY_SAT, + WEEKDAY_SUN, + _WEEKDAY_MAX, + _WEEKDAY_INVALID = -EINVAL, +}; + /* We assume a maximum timezone length of 6. TZNAME_MAX is not defined on Linux, but glibc internally initializes this * to 6. Let's rely on that. */ #define FORMAT_TIMESTAMP_MAX (3U+1U+10U+1U+8U+1U+6U+1U+6U+1U) @@ -125,6 +137,10 @@ char* format_timestamp_style(char *buf, size_t l, usec_t t, TimestampStyle style char* format_timestamp_relative_full(char *buf, size_t l, usec_t t, clockid_t clock, bool implicit_left) _warn_unused_result_; char* format_timespan(char *buf, size_t l, usec_t t, usec_t accuracy) _warn_unused_result_; +/* Returns the abbreviated English weekday name for wd in Mon=0 … Sun=6 order + * (matching systemd's weekdays_bits layout). */ +const char* weekday_to_string(int i); + _warn_unused_result_ static inline char* format_timestamp_relative(char *buf, size_t l, usec_t t) { return format_timestamp_relative_full(buf, l, t, CLOCK_REALTIME, /* implicit_left= */ false); diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index c9e270a6682fa..bf17e2df46f01 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -2082,6 +2082,14 @@ int config_parse_timer( log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse calendar specification, ignoring: %s", k); return 0; } + + int wday; + if (calendar_spec_weekday_conflicts(c, &wday)) + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Weekday constraint does not match the fixed date %04d-%02d-%02d " + "(which is a %s), so this timer will never elapse.", + c->year->start, c->month->start, c->day->start, + weekday_to_string(wday)); } else { r = parse_sec(k, &usec); if (r < 0) { diff --git a/src/run/run.c b/src/run/run.c index 1fca68c76f84b..068e0cc29feaf 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -552,19 +552,24 @@ static int parse_argv(int argc, char *argv[]) { OPTION_LONG("on-calendar", "SPEC", "Realtime timer"): { _cleanup_(calendar_spec_freep) CalendarSpec *cs = NULL; - r = calendar_spec_from_string(opts.arg, &cs); + r = calendar_spec_from_string_full(opts.arg, &cs, /* warn_on_weekday_mismatch= */ true); if (r < 0) return log_error_errno(r, "Failed to parse calendar event specification: %m"); /* Let's make sure the given calendar event is not in the past */ r = calendar_spec_next_usec(cs, now(CLOCK_REALTIME), NULL); - if (r == -ENOENT) - /* The calendar event is in the past — let's warn about this, but install it - * anyway as is. The service manager will trigger the service right away. - * Moreover, the server side might have a different clock or timezone than we - * do, hence it should decide when or whether to run something. */ - log_warning("Specified calendar expression is in the past, proceeding anyway."); - else if (r < 0) + if (r == -ENOENT) { + /* The calendar event might be in the past, so let's warn about this, but + * install it anyway as is. The service manager will trigger the service + * right away. Moreover, the server side might have a different clock or + * timezone than we do, hence it should decide when or whether to run + * something. + * + * However, a mismatching weekday for a fixed date also results in -ENOENT, + * and was already warned about when parsing. */ + if (!calendar_spec_weekday_conflicts(cs, NULL)) + log_warning("Specified calendar expression is in the past, proceeding anyway."); + } else if (r < 0) return log_error_errno(r, "Failed to calculate next time calendar expression elapses: %m"); r = add_timer_property("OnCalendar", opts.arg); diff --git a/src/shared/calendarspec.c b/src/shared/calendarspec.c index 225d811d19280..6a56438c1c7ba 100644 --- a/src/shared/calendarspec.c +++ b/src/shared/calendarspec.c @@ -15,7 +15,7 @@ #include "strv.h" #include "time-util.h" -#define BITS_WEEKDAYS 127 +#define BITS_WEEKDAYS ((1 << _WEEKDAY_MAX) - 1) #define MIN_YEAR 1970 #define MAX_YEAR 2199 @@ -237,16 +237,6 @@ _pure_ bool calendar_spec_valid(CalendarSpec *c) { } static void format_weekdays(FILE *f, const CalendarSpec *c) { - static const char *const days[] = { - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }; - int l, x; bool need_comma = false; @@ -254,7 +244,7 @@ static void format_weekdays(FILE *f, const CalendarSpec *c) { assert(c); assert(c->weekdays_bits > 0 && c->weekdays_bits <= BITS_WEEKDAYS); - for (x = 0, l = -1; x < (int) ELEMENTSOF(days); x++) { + for (x = 0, l = -1; x < _WEEKDAY_MAX; x++) { if (c->weekdays_bits & (1 << x)) { @@ -264,7 +254,7 @@ static void format_weekdays(FILE *f, const CalendarSpec *c) { else need_comma = true; - fputs(days[x], f); + fputs(weekday_to_string(x), f); l = x; } @@ -272,7 +262,7 @@ static void format_weekdays(FILE *f, const CalendarSpec *c) { if (x > l + 1) { fputs(x > l + 2 ? ".." : ",", f); - fputs(days[x-1], f); + fputs(weekday_to_string(x-1), f); } l = -1; @@ -281,7 +271,7 @@ static void format_weekdays(FILE *f, const CalendarSpec *c) { if (l >= 0 && x > l + 1) { fputs(x > l + 2 ? ".." : ",", f); - fputs(days[x-1], f); + fputs(weekday_to_string(x-1), f); } } @@ -877,7 +867,7 @@ static int parse_calendar_time(const char **p, CalendarSpec *c) { return 0; } -int calendar_spec_from_string(const char *p, CalendarSpec **ret) { +int calendar_spec_from_string_full(const char *p, CalendarSpec **ret, bool warn_on_weekday_mismatch) { const char *utc; _cleanup_(calendar_spec_freep) CalendarSpec *c = NULL; _cleanup_free_ char *p_tmp = NULL; @@ -1098,6 +1088,15 @@ int calendar_spec_from_string(const char *p, CalendarSpec **ret) { if (!calendar_spec_valid(c)) return -EINVAL; + if (warn_on_weekday_mismatch) { + int wday; + if (calendar_spec_weekday_conflicts(c, &wday)) + log_warning("Weekday constraint does not match the fixed date %04d-%02d-%02d " + "(which is a %s), so this timer will never elapse.", + c->year->start, c->month->start, c->day->start, + weekday_to_string(wday)); + } + if (ret) *ret = TAKE_PTR(c); return 0; @@ -1228,6 +1227,7 @@ static int tm_within_bounds(struct tm *tm, bool utc) { static bool matches_weekday(int weekdays_bits, const struct tm *tm, bool utc) { struct tm t; + usec_t usec; int k; assert(tm); @@ -1236,13 +1236,64 @@ static bool matches_weekday(int weekdays_bits, const struct tm *tm, bool utc) { return true; t = *tm; - if (mktime_or_timegm_usec(&t, utc, /* ret= */ NULL) < 0) + if (mktime_or_timegm_usec(&t, utc, &usec) < 0) + return false; + if (localtime_or_gmtime_usec(usec, utc, &t) < 0) return false; k = t.tm_wday == 0 ? 6 : t.tm_wday - 1; return (weekdays_bits & (1 << k)); } +static bool component_is_single_fixed(const CalendarComponent *c) { + /* Returns true if the component is a single fixed value: no range, no repeat, no alternatives. */ + return c && !c->next && c->repeat == 0 && (c->stop < 0 || c->stop == c->start); +} + +bool calendar_spec_weekday_conflicts(const CalendarSpec *spec, int *ret_actual_wday) { + assert(spec); + + if (spec->weekdays_bits <= 0 || spec->weekdays_bits >= BITS_WEEKDAYS) + return false; + + /* Skip when end_of_month (~) is used: day->start holds an offset, not an + * absolute day-of-month, so the real firing date cannot be determined here. */ + if (spec->end_of_month) + return false; + + /* Only detectable when year, month and day are each a single fixed value */ + if (!component_is_single_fixed(spec->year) || + !component_is_single_fixed(spec->month) || + !component_is_single_fixed(spec->day)) + return false; + + /* CalendarSpec stores month as 1-12; struct tm uses 0-11 */ + struct tm tm = { + .tm_year = spec->year->start - 1900, + .tm_mon = spec->month->start - 1, + .tm_mday = spec->day->start, + }; + struct tm orig = tm; + + /* Compute weekday in UTC to avoid TZ/DST skew; reject dates that would be + * silently normalised (e.g. 2027-02-31 → 2027-03-03). + * + * Do not rely on timegm() to fill tm_wday, since musl does not do that. */ + usec_t usec; + if (mktime_or_timegm_usec(&tm, /* utc= */ true, &usec) < 0 || + localtime_or_gmtime_usec(usec, /* utc= */ true, &tm) < 0 || + tm.tm_year != orig.tm_year || tm.tm_mon != orig.tm_mon || + tm.tm_mday != orig.tm_mday) + return false; + + if (matches_weekday(spec->weekdays_bits, &tm, /* utc= */ true)) + return false; /* no conflict */ + + if (ret_actual_wday) + *ret_actual_wday = tm.tm_wday == 0 ? 6 : tm.tm_wday - 1; + return true; +} + static int tm_compare(const struct tm *t1, const struct tm *t2) { int r; diff --git a/src/shared/calendarspec.h b/src/shared/calendarspec.h index ce76a7c1c6240..339dda30abea6 100644 --- a/src/shared/calendarspec.h +++ b/src/shared/calendarspec.h @@ -35,8 +35,13 @@ CalendarSpec* calendar_spec_free(CalendarSpec *c); bool calendar_spec_valid(CalendarSpec *spec); int calendar_spec_to_string(const CalendarSpec *spec, char **ret); -int calendar_spec_from_string(const char *p, CalendarSpec **ret); +int calendar_spec_from_string_full(const char *p, CalendarSpec **ret, bool warn_on_weekday_mismatch); + +static inline int calendar_spec_from_string(const char *p, CalendarSpec **ret) { + return calendar_spec_from_string_full(p, ret, /* warn_on_weekday_mismatch= */ false); +} int calendar_spec_next_usec(const CalendarSpec *spec, usec_t usec, usec_t *next); +bool calendar_spec_weekday_conflicts(const CalendarSpec *spec, int *ret_actual_wday); DEFINE_TRIVIAL_CLEANUP_FUNC(CalendarSpec*, calendar_spec_free); diff --git a/src/test/test-calendarspec.c b/src/test/test-calendarspec.c index 6c901b1893c57..0d5171856116d 100644 --- a/src/test/test-calendarspec.c +++ b/src/test/test-calendarspec.c @@ -7,6 +7,11 @@ #include "calendarspec.h" #include "env-util.h" #include "errno-util.h" +#include "fd-util.h" +#include "io-util.h" +#include "log.h" +#include "memfd-util.h" +#include "process-util.h" #include "string-util.h" #include "tests.h" #include "time-util.h" @@ -256,6 +261,76 @@ TEST(calendar_spec_from_string) { assert_se(calendar_spec_from_string("*:4,30:*\n", &c) == -EINVAL); } +/* Fork a child with stderr redirected to a memfd, invoke calendar_spec_from_string_full() + * with warn=true, wait, then return the memfd rewound to 0. */ +static int run_in_child_capture_stderr(const char *spec) { + _cleanup_close_ int memfd = memfd_new("calendarspec-warn-test"); + assert_se(memfd >= 0); + + int r = pidref_safe_fork_full( + "(calendarspec-warn)", + (const int[3]) { -1, -1, memfd }, + NULL, 0, + FORK_WAIT|FORK_LOG|FORK_REARRANGE_STDIO, + NULL); + assert_se(r >= 0); + + if (r == 0) { + /* log.c may have inherited a console fd that no longer points at stderr. */ + log_close(); + log_set_target_and_open(LOG_TARGET_CONSOLE); + log_set_max_level(LOG_WARNING); + + _cleanup_(calendar_spec_freep) CalendarSpec *c = NULL; + (void) calendar_spec_from_string_full(spec, &c, /* warn_on_weekday_mismatch= */ true); + /* Write PARSE_OK to stderr as a positive anchor. */ + fputs("PARSE_OK\n", stderr); + _exit(0); + } + + assert_se(lseek(memfd, 0, SEEK_SET) == 0); + return TAKE_FD(memfd); +} + +TEST(calendar_spec_weekday_conflict) { + char buf[4096]; + ssize_t n; + + /* Case 1: conflicting weekday — warning must be emitted */ + { + _cleanup_close_ int fd = run_in_child_capture_stderr("Thu 2027-01-01"); + n = loop_read(fd, buf, sizeof(buf) - 1, /* do_poll= */ false); + assert_se(n > 0); + buf[n] = '\0'; + /* Check date, weekday text and warning phrase are present. */ + assert_se(strstr(buf, "2027-01-01")); + assert_se(strstr(buf, "which is a Fri")); + assert_se(strstr(buf, "will never elapse")); + } + + /* Case 2: correct weekday — no warning */ + { + _cleanup_close_ int fd = run_in_child_capture_stderr("Fri 2027-01-01"); + n = loop_read(fd, buf, sizeof(buf) - 1, /* do_poll= */ false); + /* Ensure child wrote PARSE_OK. */ + assert_se(n >= 0); + buf[MAX(n, (ssize_t)0)] = '\0'; + assert_se(strstr(buf, "PARSE_OK")); + assert_se(!strstr(buf, "will never elapse")); + } + + /* Case 3: wildcard date — no static conflict, no warning */ + { + _cleanup_close_ int fd = run_in_child_capture_stderr("Thu *-*-*"); + n = loop_read(fd, buf, sizeof(buf) - 1, /* do_poll= */ false); + /* Ensure child wrote PARSE_OK. */ + assert_se(n >= 0); + buf[MAX(n, (ssize_t)0)] = '\0'; + assert_se(strstr(buf, "PARSE_OK")); + assert_se(!strstr(buf, "will never elapse")); + } +} + static int intro(void) { /* Tests have hard-coded results that do not expect a specific timezone to be set by the caller */ ASSERT_OK_ERRNO(unsetenv("TZ")); From b7769aa34eee5abcdb0ede535459cb9d42fc4376 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 00:05:41 +0100 Subject: [PATCH 31/71] machined: drop superfluos 'supervisor' varlink input parameter for register method The supervisor is derived from the caller's socket in D-Bus, and it is not an input parameter. Do the same in varlink. Follow-up for 97754cd14dc7b3630585383ecba92191667860e4 --- src/machine/machine-varlink.c | 14 +++++++------- src/shared/varlink-io.systemd.Machine.c | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/machine/machine-varlink.c b/src/machine/machine-varlink.c index a73f81b6f7225..a801978fafd0a 100644 --- a/src/machine/machine-varlink.c +++ b/src/machine/machine-varlink.c @@ -126,6 +126,7 @@ static int machine_cid(const char *name, sd_json_variant *variant, sd_json_dispa int vl_method_register(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { Manager *manager = ASSERT_PTR(userdata); _cleanup_(machine_freep) Machine *machine = NULL; + const char *bad_field = NULL; bool sender_is_admin = false; int r; @@ -136,8 +137,6 @@ int vl_method_register(sd_varlink *link, sd_json_variant *parameters, sd_varlink { "class", SD_JSON_VARIANT_STRING, dispatch_machine_class, offsetof(Machine, class), SD_JSON_MANDATORY }, { "leader", _SD_JSON_VARIANT_TYPE_INVALID, machine_pidref, offsetof(Machine, leader), SD_JSON_STRICT }, { "leaderProcessId", SD_JSON_VARIANT_OBJECT, machine_pidref, offsetof(Machine, leader), SD_JSON_STRICT }, - { "supervisor", _SD_JSON_VARIANT_TYPE_INVALID, machine_pidref, offsetof(Machine, supervisor), SD_JSON_STRICT }, - { "supervisorProcessId", SD_JSON_VARIANT_OBJECT, machine_pidref, offsetof(Machine, supervisor), SD_JSON_STRICT }, { "rootDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(Machine, root_directory), SD_JSON_STRICT }, { "ifIndices", SD_JSON_VARIANT_ARRAY, machine_ifindices, 0, 0 }, { "vSockCid", _SD_JSON_VARIANT_TYPE_INVALID, machine_cid, offsetof(Machine, vsock_cid), 0 }, @@ -153,9 +152,12 @@ int vl_method_register(sd_varlink *link, sd_json_variant *parameters, sd_varlink if (r < 0) return r; - r = sd_varlink_dispatch(link, parameters, dispatch_table, machine); - if (r != 0) + r = sd_json_dispatch_full(parameters, dispatch_table, /* bad= */ NULL, SD_JSON_ALLOW_EXTENSIONS, machine, &bad_field); + if (r < 0) { + if (bad_field) + return sd_varlink_error_invalid_parameter_name(link, bad_field); return r; + } if (!MACHINE_CLASS_CAN_REGISTER(machine->class)) return sd_varlink_error_invalid_parameter_name(link, "class"); @@ -179,9 +181,7 @@ int vl_method_register(sd_varlink *link, sd_json_variant *parameters, sd_varlink r = varlink_get_peer_pidref(link, &machine->leader); if (r < 0) return r; - } - - if (!pidref_is_set(&machine->supervisor)) { + } else { _cleanup_(pidref_done) PidRef client_pidref = PIDREF_NULL; r = varlink_get_peer_pidref(link, &client_pidref); diff --git a/src/shared/varlink-io.systemd.Machine.c b/src/shared/varlink-io.systemd.Machine.c index cb1b0665d6092..76bcf7b3f39cc 100644 --- a/src/shared/varlink-io.systemd.Machine.c +++ b/src/shared/varlink-io.systemd.Machine.c @@ -48,10 +48,10 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_INPUT(leader, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The leader PID as ProcessId structure. If both the leader and leaderProcessId parameters are specified they must reference the same process. Typically one would only specify one or the other however. It's generally recommended to specify leaderProcessId as it references a process in a robust way without risk of identifier recycling."), SD_VARLINK_DEFINE_INPUT_BY_TYPE(leaderProcessId, ProcessId, SD_VARLINK_NULLABLE), - SD_VARLINK_FIELD_COMMENT("The supervisor PID as simple positive integer."), - SD_VARLINK_DEFINE_INPUT(supervisor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), - SD_VARLINK_FIELD_COMMENT("The supervisor PID as ProcessId structure. If both the supervisor and supervisorProcessId parameters are specified they must reference the same process. Typically only one or the other would be specified. It's generally recommended to specify supervisorProcessId as it references a process in a robust way without risk of identifier recycling."), - SD_VARLINK_DEFINE_INPUT_BY_TYPE(supervisorProcessId, ProcessId, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The supervisor PID as simple positive integer. Deprecated and ignored: the supervisor is now always derived from the caller's socket (kept for backward compatibility)."), + SD_VARLINK_DEFINE_INPUT(supervisor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), /* for backward compat, ignored */ + SD_VARLINK_FIELD_COMMENT("The supervisor PID as ProcessId structure. Deprecated and ignored: the supervisor is now always derived from the caller's socket (kept for backward compatibility)."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(supervisorProcessId, ProcessId, SD_VARLINK_NULLABLE), /* for backward compat, ignored */ SD_VARLINK_DEFINE_INPUT(rootDirectory, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_INPUT(ifIndices, SD_VARLINK_INT, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_INPUT(vSockCid, SD_VARLINK_INT, SD_VARLINK_NULLABLE), From 193749a52e9b09a5d326ced3b9eabf64da03d164 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 07:39:24 +0900 Subject: [PATCH 32/71] journal: move audit-type.[ch] and related generators to src/journal It is only used by systemd-journald and own unit test. --- .../sd-journal => journal}/audit-type.c | 0 .../sd-journal => journal}/audit-type.h | 0 .../audit_type-to-name.awk | 0 .../generate-audit_type-list.sh | 0 src/journal/meson.build | 20 ++++++++++++++ .../sd-journal => journal}/test-audit-type.c | 0 src/libsystemd/meson.build | 7 ----- src/libsystemd/sd-journal/meson.build | 26 ------------------- 8 files changed, 20 insertions(+), 33 deletions(-) rename src/{libsystemd/sd-journal => journal}/audit-type.c (100%) rename src/{libsystemd/sd-journal => journal}/audit-type.h (100%) rename src/{libsystemd/sd-journal => journal}/audit_type-to-name.awk (100%) rename src/{libsystemd/sd-journal => journal}/generate-audit_type-list.sh (100%) rename src/{libsystemd/sd-journal => journal}/test-audit-type.c (100%) delete mode 100644 src/libsystemd/sd-journal/meson.build diff --git a/src/libsystemd/sd-journal/audit-type.c b/src/journal/audit-type.c similarity index 100% rename from src/libsystemd/sd-journal/audit-type.c rename to src/journal/audit-type.c diff --git a/src/libsystemd/sd-journal/audit-type.h b/src/journal/audit-type.h similarity index 100% rename from src/libsystemd/sd-journal/audit-type.h rename to src/journal/audit-type.h diff --git a/src/libsystemd/sd-journal/audit_type-to-name.awk b/src/journal/audit_type-to-name.awk similarity index 100% rename from src/libsystemd/sd-journal/audit_type-to-name.awk rename to src/journal/audit_type-to-name.awk diff --git a/src/libsystemd/sd-journal/generate-audit_type-list.sh b/src/journal/generate-audit_type-list.sh similarity index 100% rename from src/libsystemd/sd-journal/generate-audit_type-list.sh rename to src/journal/generate-audit_type-list.sh diff --git a/src/journal/meson.build b/src/journal/meson.build index e39381f03f633..5ac4ae2cd077f 100644 --- a/src/journal/meson.build +++ b/src/journal/meson.build @@ -4,6 +4,7 @@ systemd_journald_sources = files( 'journald.c', ) systemd_journald_extract_sources = files( + 'audit-type.c', 'journald-audit.c', 'journald-client.c', 'journald-config.c', @@ -29,6 +30,22 @@ journald_gperf_c = custom_target( generated_sources += journald_gperf_c systemd_journald_extract_sources += journald_gperf_c +generate_audit_type_list = files('generate-audit_type-list.sh') +audit_type_list_txt = custom_target( + input : [generate_audit_type_list, audit_sources], + output : 'audit_type-list.txt', + command : [env, 'bash', generate_audit_type_list, cpp, system_include_args], + capture : true) + +audit_type_to_name = custom_target( + input : ['audit_type-to-name.awk', audit_type_list_txt], + output : 'audit_type-to-name.inc', + command : [awk, '-f', '@INPUT0@', '@INPUT1@'], + capture : true) + +generated_sources += audit_type_to_name +systemd_journald_extract_sources += audit_type_to_name + journalctl_sources = files( 'journalctl.c', 'journalctl-authenticate.c', @@ -99,6 +116,9 @@ executables += [ libzstd_cflags, ], }, + journal_test_template + { + 'sources' : files('test-audit-type.c'), + }, journal_test_template + { 'sources' : files('test-journald-config.c'), 'dependencies' : [ diff --git a/src/libsystemd/sd-journal/test-audit-type.c b/src/journal/test-audit-type.c similarity index 100% rename from src/libsystemd/sd-journal/test-audit-type.c rename to src/journal/test-audit-type.c diff --git a/src/libsystemd/meson.build b/src/libsystemd/meson.build index d1797722a42da..e393d7e86d860 100644 --- a/src/libsystemd/meson.build +++ b/src/libsystemd/meson.build @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later sd_journal_sources = files( - 'sd-journal/audit-type.c', 'sd-journal/catalog.c', 'sd-journal/journal-authenticate-internal.c', 'sd-journal/journal-file.c', @@ -13,11 +12,6 @@ sd_journal_sources = files( 'sd-journal/sd-journal.c', ) -subdir('sd-journal') - -generated_sources += audit_type_to_name -sd_journal_sources += audit_type_to_name - ############################################################ sd_id128_sources = files( @@ -197,7 +191,6 @@ simple_tests += files( 'sd-future/test-fiber-ops.c', 'sd-hwdb/test-sd-hwdb.c', 'sd-id128/test-id128.c', - 'sd-journal/test-audit-type.c', 'sd-journal/test-catalog.c', 'sd-journal/test-journal-file.c', 'sd-journal/test-journal-flush.c', diff --git a/src/libsystemd/sd-journal/meson.build b/src/libsystemd/sd-journal/meson.build deleted file mode 100644 index 61ddf66b59c35..0000000000000 --- a/src/libsystemd/sd-journal/meson.build +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -# We're generating a header file here intended to be included -# by audit-type.c. To make that work, we have to add the generated -# header file's directory to the include directories for libsystemd. -# In meson, adding include directories is done via include_directories(), -# which always adds two include directories for each argument, one relative to -# the source directory and one relative to the build directory. We don't -# want to add src/libsystemd to the include directories, yet custom_target() -# does not allow path segments in its output argument, so to make sure the -# generated header is written to src/libsystemd/sd-journal in the build directory, -# the custom_target() has to be defined here in src/libsystemd/sd-journal -# in the source directory. - -generate_audit_type_list = files('generate-audit_type-list.sh') -audit_type_list_txt = custom_target( - input : [generate_audit_type_list, audit_sources], - output : 'audit_type-list.txt', - command : [env, 'bash', generate_audit_type_list, cpp, system_include_args], - capture : true) - -audit_type_to_name = custom_target( - input : ['audit_type-to-name.awk', audit_type_list_txt], - output : 'audit_type-to-name.inc', - command : [awk, '-f', '@INPUT0@', '@INPUT1@'], - capture : true) From d04fbcf956a7454b9626c668df5208ca6b5cf385 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Thu, 2 Jul 2026 10:30:55 +0000 Subject: [PATCH 33/71] tree-wide: get rid of backslashes in file names File names containing backslashes cannot be checked out on Windows, are not handled properly by build systems such as buck, and are awkward to work with in general, so store the \x2d slice units under sanitized file names, and rename them to the real unit name on install via a new optional "name" key in the units list. The fuzz-unit-file corpus sample is renamed as well; its file name is not meaningful to the fuzzer, and dropping the backslash means it is no longer skipped by the meson workaround for https://github.com/mesonbuild/meson/issues/1564, so it runs as a regression test again. --- .../fuzz/fuzz-unit-file/dm-backslash.swap | 0 units/meson.build | 15 ++++++++++++--- .../system-systemd-cryptsetup.slice | 0 .../system-systemd-mute-console.slice | 0 .../system-systemd-veritysetup.slice | 0 5 files changed, 12 insertions(+), 3 deletions(-) rename "test/fuzz/fuzz-unit-file/dm-back\\x2dslash.swap" => test/fuzz/fuzz-unit-file/dm-backslash.swap (100%) rename "units/system-systemd\\x2dcryptsetup.slice" => units/system-systemd-cryptsetup.slice (100%) rename "units/system-systemd\\x2dmute\\x2dconsole.slice" => units/system-systemd-mute-console.slice (100%) rename "units/system-systemd\\x2dveritysetup.slice" => units/system-systemd-veritysetup.slice (100%) diff --git "a/test/fuzz/fuzz-unit-file/dm-back\\x2dslash.swap" b/test/fuzz/fuzz-unit-file/dm-backslash.swap similarity index 100% rename from "test/fuzz/fuzz-unit-file/dm-back\\x2dslash.swap" rename to test/fuzz/fuzz-unit-file/dm-backslash.swap diff --git a/units/meson.build b/units/meson.build index d61138bf7b2ba..ab7989b4b9d4d 100644 --- a/units/meson.build +++ b/units/meson.build @@ -150,7 +150,10 @@ units = [ 'symlinks' : ['sockets.target.wants/'] }, { 'file' : 'systemd-mute-console@.service' }, - { 'file' : 'system-systemd\\x2dmute\\x2dconsole.slice' }, + { + 'file' : 'system-systemd-mute-console.slice', + 'name' : 'system-systemd\\x2dmute\\x2dconsole.slice', + }, { 'file' : 'network-online.target' }, { 'file' : 'network-pre.target' }, { 'file' : 'network.target' }, @@ -242,11 +245,13 @@ units = [ { 'file' : 'syslog.socket' }, { 'file' : 'system-install.target' }, { - 'file' : 'system-systemd\\x2dcryptsetup.slice', + 'file' : 'system-systemd-cryptsetup.slice', + 'name' : 'system-systemd\\x2dcryptsetup.slice', 'conditions' : ['HAVE_LIBCRYPTSETUP'], }, { - 'file' : 'system-systemd\\x2dveritysetup.slice', + 'file' : 'system-systemd-veritysetup.slice', + 'name' : 'system-systemd\\x2dveritysetup.slice', 'conditions' : ['HAVE_LIBCRYPTSETUP'], }, { 'file' : 'system-update-cleanup.service' }, @@ -1108,6 +1113,9 @@ foreach unit : units needs_jinja = false name = source endif + # Unit names that contain characters which are awkward in source file names (e.g. the + # backslash in \x2d escapes) are stored under a sanitized file name and renamed on install. + name = unit.get('name', name) source = files(source) install = true @@ -1133,6 +1141,7 @@ foreach unit : units endif elif install install_data(source, + rename : name, install_dir : systemunitdir, install_tag : unit.get('install_tag', '')) endif diff --git "a/units/system-systemd\\x2dcryptsetup.slice" b/units/system-systemd-cryptsetup.slice similarity index 100% rename from "units/system-systemd\\x2dcryptsetup.slice" rename to units/system-systemd-cryptsetup.slice diff --git "a/units/system-systemd\\x2dmute\\x2dconsole.slice" b/units/system-systemd-mute-console.slice similarity index 100% rename from "units/system-systemd\\x2dmute\\x2dconsole.slice" rename to units/system-systemd-mute-console.slice diff --git "a/units/system-systemd\\x2dveritysetup.slice" b/units/system-systemd-veritysetup.slice similarity index 100% rename from "units/system-systemd\\x2dveritysetup.slice" rename to units/system-systemd-veritysetup.slice From d06d8a232bf8ff4b27e4b5326dcec9717d89f3bc Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 17:15:38 +0100 Subject: [PATCH 34/71] boot: reject GPT headers with SizeOfPartitionEntry below the minimum Commit 0cf5f816f22c replaced the original lower-bound check if (h->SizeOfPartitionEntry < sizeof(EFI_PARTITION_ENTRY)) return false; with a multiple-of check if ((h->SizeOfPartitionEntry % sizeof(EFI_PARTITION_ENTRY)) != 0) return false; to additionally require the entry size to be a multiple of 128. The modulo test is however also satisfied by SizeOfPartitionEntry == 0, so a GPT header advertising a zero entry size now passes verify_gpt(). Restore the lower bound in addition to the multiple-of check, so the entry size must be at least sizeof(EFI_PARTITION_ENTRY) and a multiple of it (128 bytes). Follow-up for 0cf5f816f22c78740e122dfb6b3942ba4241717b --- src/boot/part-discovery.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/boot/part-discovery.c b/src/boot/part-discovery.c index ab553a6530bbc..10ba524125407 100644 --- a/src/boot/part-discovery.c +++ b/src/boot/part-discovery.c @@ -60,7 +60,8 @@ static bool verify_gpt(/* const */ GptHeader *h, EFI_LBA lba_expected) { if (h->MyLBA != lba_expected) return false; - if ((h->SizeOfPartitionEntry % sizeof(EFI_PARTITION_ENTRY)) != 0) + if (h->SizeOfPartitionEntry < sizeof(EFI_PARTITION_ENTRY) || + (h->SizeOfPartitionEntry % sizeof(EFI_PARTITION_ENTRY)) != 0) return false; if (h->NumberOfPartitionEntries <= 0 || h->NumberOfPartitionEntries > 1024) From 0d06dbabc364fe58e4bd1b9696f3c5b823620dbc Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 05:32:28 +0900 Subject: [PATCH 35/71] include: do not override kernel headers with libaudit headers Overriding to implicitly include turns out to be problematic. Because is pulled in by various other kernel headers, any source file including those kernel headers indirectly ends up depending on , even if the executable or library being built does not require libaudit at all. Let's drop the override header and include only where explicitly needed. This also moves the fallback definitions of AUDIT_SERVICE_* and MAX_AUDIT_MESSAGE_LENGTH to libaudit-util.h. --- src/core/service.c | 2 +- src/include/meson.build | 5 ----- src/include/override/linux/audit.h | 33 ------------------------------ src/journal/audit_type-to-name.awk | 4 ++++ src/journal/journald-manager.c | 2 +- src/journal/meson.build | 13 ++++++++++-- src/shared/libaudit-util.h | 20 ++++++++++++++++++ 7 files changed, 37 insertions(+), 42 deletions(-) delete mode 100644 src/include/override/linux/audit.h diff --git a/src/core/service.c b/src/core/service.c index 9e4af299f7ec9..012336b47a0ba 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include /* IWYU pragma: keep */ #include #include #include @@ -36,6 +35,7 @@ #include "glyph-util.h" #include "id128-util.h" #include "image-policy.h" +#include "libaudit-util.h" /* IWYU pragma: keep */ #include "log.h" #include "luo-util.h" #include "manager.h" diff --git a/src/include/meson.build b/src/include/meson.build index 9cf7e4dbc8895..5fc8bb6a61e2b 100644 --- a/src/include/meson.build +++ b/src/include/meson.build @@ -33,11 +33,6 @@ ipproto_sources = files( 'uapi/linux/in.h', ) -# Source files that provides AUDIT_XYZ -audit_sources = files( - 'override/linux/audit.h', -) - # Source files that provides KEY_XYZ keyboard_sources = files( 'uapi/linux/input.h', diff --git a/src/include/override/linux/audit.h b/src/include/override/linux/audit.h deleted file mode 100644 index abdd25b37921a..0000000000000 --- a/src/include/override/linux/audit.h +++ /dev/null @@ -1,33 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ -#pragma once - -#include_next /* IWYU pragma: export */ - -#if HAVE_AUDIT -# include /* IWYU pragma: export */ -#endif - -#include - -/* We use static_assert() directly here instead of assert_cc() - * because if we include macro.h in this header, the invocation - * of generate-audit_type-list.sh becomes more complex. - */ - -#ifndef AUDIT_SERVICE_START -# define AUDIT_SERVICE_START 1130 /* Service (daemon) start */ -#else -static_assert(AUDIT_SERVICE_START == 1130, ""); -#endif - -#ifndef AUDIT_SERVICE_STOP -# define AUDIT_SERVICE_STOP 1131 /* Service (daemon) stop */ -#else -static_assert(AUDIT_SERVICE_STOP == 1131, ""); -#endif - -#ifndef MAX_AUDIT_MESSAGE_LENGTH -# define MAX_AUDIT_MESSAGE_LENGTH 8970 -#else -static_assert(MAX_AUDIT_MESSAGE_LENGTH == 8970, ""); -#endif diff --git a/src/journal/audit_type-to-name.awk b/src/journal/audit_type-to-name.awk index 5dbba45af1bc5..ef834ff15eaf1 100644 --- a/src/journal/audit_type-to-name.awk +++ b/src/journal/audit_type-to-name.awk @@ -1,6 +1,10 @@ # SPDX-License-Identifier: LGPL-2.1-or-later BEGIN{ + print "#if HAVE_AUDIT" + print "# include " + print "#endif" + print "" print "#include " print "" print "#include \"audit-type.h\"" diff --git a/src/journal/journald-manager.c b/src/journal/journald-manager.c index 68f95ea96782e..be3532f1ed5d6 100644 --- a/src/journal/journald-manager.c +++ b/src/journal/journald-manager.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include #include @@ -47,6 +46,7 @@ #include "journald-sync.h" #include "journald-syslog.h" #include "journald-varlink.h" +#include "libaudit-util.h" #include "log.h" #include "log-ratelimit.h" #include "memory-util.h" diff --git a/src/journal/meson.build b/src/journal/meson.build index 5ac4ae2cd077f..3579265a11931 100644 --- a/src/journal/meson.build +++ b/src/journal/meson.build @@ -31,10 +31,19 @@ generated_sources += journald_gperf_c systemd_journald_extract_sources += journald_gperf_c generate_audit_type_list = files('generate-audit_type-list.sh') +generate_audit_type_command = [env, 'bash', generate_audit_type_list, cpp] +if libaudit.found() + generate_audit_type_command += [ + '-include', 'libaudit.h', + '-I' + libaudit.get_variable('includedir'), + ] +endif +generate_audit_type_command += system_include_args + audit_type_list_txt = custom_target( - input : [generate_audit_type_list, audit_sources], + input : [generate_audit_type_list], output : 'audit_type-list.txt', - command : [env, 'bash', generate_audit_type_list, cpp, system_include_args], + command : generate_audit_type_command, capture : true) audit_type_to_name = custom_target( diff --git a/src/shared/libaudit-util.h b/src/shared/libaudit-util.h index 8bb05a2fbd3c1..75cef6f1a04f9 100644 --- a/src/shared/libaudit-util.h +++ b/src/shared/libaudit-util.h @@ -15,6 +15,26 @@ extern DLSYM_PROTOTYPE(audit_log_user_avc_message); extern DLSYM_PROTOTYPE(audit_log_user_comm_message); #endif +/* libaudit.h defines these constants. + * Define them here too so they can be used even when libaudit.h is unavailable. */ +#ifndef AUDIT_SERVICE_START +# define AUDIT_SERVICE_START 1130 /* Service (daemon) start */ +#else +assert_cc(AUDIT_SERVICE_START == 1130); +#endif + +#ifndef AUDIT_SERVICE_STOP +# define AUDIT_SERVICE_STOP 1131 /* Service (daemon) stop */ +#else +assert_cc(AUDIT_SERVICE_STOP == 1131); +#endif + +#ifndef MAX_AUDIT_MESSAGE_LENGTH +# define MAX_AUDIT_MESSAGE_LENGTH 8970 +#else +assert_cc(MAX_AUDIT_MESSAGE_LENGTH == 8970); +#endif + bool use_audit(void); int close_audit_fd(int fd); From 98b875b2acfcbcb5befe030747db942974c560b2 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Thu, 25 Jun 2026 18:33:49 +0200 Subject: [PATCH 36/71] sysinstall: Look for valid kernel image before installing Instead of failing the installation after creating the partitions and doing almost the entire installation, and then failing if we don't have a kernel image. Look for it before we start doing modifications to the disk. This way we tell the user as soon as possible that we can't install the OS because of the missing kernel image. --- src/sysinstall/sysinstall.c | 43 ++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c index 01e8cd048337d..a9aee0535f925 100644 --- a/src/sysinstall/sysinstall.c +++ b/src/sysinstall/sysinstall.c @@ -930,12 +930,16 @@ static int invoke_bootctl_link( sd_varlink **link, const char *root_dir, int root_fd, + const char *kernel_filename, + int kernel_fd, char **encrypted_credentials) { int r; assert(link); assert(root_dir); assert(root_fd >= 0); + assert(kernel_filename); + assert(kernel_fd >= 0); r = connect_to_bootctl(link); if (r < 0) @@ -959,25 +963,6 @@ static int invoke_bootctl_link( if (root_fd_idx < 0) return log_error_errno(root_fd_idx, "Failed to submit root fd onto Varlink connection: %m"); - _cleanup_free_ char *kernel_filename = NULL; - _cleanup_close_ int kernel_fd = -EBADF; - if (arg_kernel_image) { - r = path_extract_filename(arg_kernel_image, &kernel_filename); - if (r < 0) - return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", arg_kernel_image); - if (r == O_DIRECTORY) - return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Kernel path '%s' refers to directory, must be regular file, refusing.", arg_kernel_image); - - kernel_fd = xopenat_full(XAT_FDROOT, arg_kernel_image, O_RDONLY|O_CLOEXEC, XO_REGULAR, MODE_INVALID); - if (kernel_fd < 0) - return log_error_errno(kernel_fd, "Failed to open kernel image '%s': %m", arg_kernel_image); - - } else { - r = find_current_kernel(&kernel_filename, &kernel_fd); - if (r < 0) - return r; - } - int kernel_fd_idx = sd_varlink_push_dup_fd(*link, kernel_fd); if (kernel_fd_idx < 0) return log_error_errno(kernel_fd_idx, "Failed to submit kernel fd onto Varlink connection: %m"); @@ -1259,6 +1244,8 @@ static void end_marker(void) { } static int run(int argc, char *argv[]) { + _cleanup_free_ char *kernel_filename = NULL; + _cleanup_close_ int kernel_fd = -EBADF; int r; setlocale(LC_ALL, ""); @@ -1341,6 +1328,22 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; + if (arg_kernel_image) { + r = path_extract_filename(arg_kernel_image, &kernel_filename); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", arg_kernel_image); + if (r == O_DIRECTORY) + return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Kernel path '%s' refers to directory, must be regular file, refusing.", arg_kernel_image); + + kernel_fd = xopenat_full(XAT_FDROOT, arg_kernel_image, O_RDONLY|O_CLOEXEC, XO_REGULAR, MODE_INVALID); + if (kernel_fd < 0) + return log_error_errno(kernel_fd, "Failed to open kernel image '%s': %m", arg_kernel_image); + } else { + r = find_current_kernel(&kernel_filename, &kernel_fd); + if (r < 0) + return r; + } + /* Verify we have everything we need */ assert(arg_node); assert(arg_erase >= 0); @@ -1408,7 +1411,7 @@ static int run(int argc, char *argv[]) { emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *bootctl_link = NULL; - r = invoke_bootctl_link(&bootctl_link, root_dir, root_fd, encrypted_credentials); + r = invoke_bootctl_link(&bootctl_link, root_dir, root_fd, kernel_filename, kernel_fd, encrypted_credentials); if (r < 0) return r; From c17d0c7e8505c985dfe8581a7fba1b3edf760d7f Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Fri, 26 Jun 2026 18:48:23 +0200 Subject: [PATCH 37/71] sysinstall: Add varlink interface Allow graphical installers to use system sysinstall. This will be used by gnome-setup to install GNOME OS and ideally installers of other distributions talk to the same varlink interface. --- src/shared/meson.build | 1 + src/shared/varlink-io.systemd.Repart.c | 10 +- src/shared/varlink-io.systemd.Repart.h | 6 + src/shared/varlink-io.systemd.SysInstall.c | 125 +++ src/shared/varlink-io.systemd.SysInstall.h | 6 + src/sysinstall/io.systemd.sysinstall.policy | 40 + src/sysinstall/meson.build | 5 + src/sysinstall/sysinstall.c | 1046 ++++++++++++++++--- units/meson.build | 9 + units/systemd-sysinstall.service | 2 + units/systemd-sysinstall.socket | 27 + units/systemd-sysinstall@.service | 20 + 12 files changed, 1152 insertions(+), 145 deletions(-) create mode 100644 src/shared/varlink-io.systemd.SysInstall.c create mode 100644 src/shared/varlink-io.systemd.SysInstall.h create mode 100644 src/sysinstall/io.systemd.sysinstall.policy create mode 100644 units/systemd-sysinstall.socket create mode 100644 units/systemd-sysinstall@.service diff --git a/src/shared/meson.build b/src/shared/meson.build index e2bef83bd2672..09563468aee5a 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -251,6 +251,7 @@ shared_sources = files( 'varlink-io.systemd.Resolve.Monitor.c', 'varlink-io.systemd.Shutdown.c', 'varlink-io.systemd.StorageProvider.c', + 'varlink-io.systemd.SysInstall.c', 'varlink-io.systemd.SysUpdate.c', 'varlink-io.systemd.SysUpdate.Notify.c', 'varlink-io.systemd.Udev.c', diff --git a/src/shared/varlink-io.systemd.Repart.c b/src/shared/varlink-io.systemd.Repart.c index fcfdcd3d650a8..968d643f017c0 100644 --- a/src/shared/varlink-io.systemd.Repart.c +++ b/src/shared/varlink-io.systemd.Repart.c @@ -29,7 +29,7 @@ static SD_VARLINK_DEFINE_ENUM_TYPE( SD_VARLINK_FIELD_COMMENT("Always create a new partition table, potentially overwriting an existing table"), SD_VARLINK_DEFINE_ENUM_VALUE(force)); -static SD_VARLINK_DEFINE_ENUM_TYPE( +SD_VARLINK_DEFINE_ENUM_TYPE( BlockDeviceAction, SD_VARLINK_FIELD_COMMENT("The device is currently present and a candidate. Emitted both during the initial enumeration and for live uevents (any action other than 'remove' is propagated as 'add', including the synthesized transitions when a device becomes empty or read-only and a relevant ignore* input is in effect)."), SD_VARLINK_DEFINE_ENUM_VALUE(add), @@ -93,9 +93,9 @@ static SD_VARLINK_DEFINE_METHOD_FULL( SD_VARLINK_DEFINE_OUTPUT(subsystem, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); -static SD_VARLINK_DEFINE_ERROR(NoCandidateDevices); -static SD_VARLINK_DEFINE_ERROR(ConflictingDiskLabelPresent); -static SD_VARLINK_DEFINE_ERROR( +SD_VARLINK_DEFINE_ERROR(NoCandidateDevices); +SD_VARLINK_DEFINE_ERROR(ConflictingDiskLabelPresent); +SD_VARLINK_DEFINE_ERROR( InsufficientFreeSpace, SD_VARLINK_FIELD_COMMENT("Minimal size of the disk required for the installation."), SD_VARLINK_DEFINE_FIELD(minimalSizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE), @@ -103,7 +103,7 @@ static SD_VARLINK_DEFINE_ERROR( SD_VARLINK_DEFINE_FIELD(needFreeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Size of the selected block device."), SD_VARLINK_DEFINE_FIELD(currentSizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); -static SD_VARLINK_DEFINE_ERROR( +SD_VARLINK_DEFINE_ERROR( DiskTooSmall, SD_VARLINK_FIELD_COMMENT("Minimal size of the disk required for the installation."), SD_VARLINK_DEFINE_FIELD(minimalSizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE), diff --git a/src/shared/varlink-io.systemd.Repart.h b/src/shared/varlink-io.systemd.Repart.h index 7c7cb7dd79f86..536ea88ccdd98 100644 --- a/src/shared/varlink-io.systemd.Repart.h +++ b/src/shared/varlink-io.systemd.Repart.h @@ -4,3 +4,9 @@ #include "sd-varlink-idl.h" extern const sd_varlink_interface vl_interface_io_systemd_Repart; +extern const sd_varlink_symbol vl_type_BlockDeviceAction; + +extern const sd_varlink_symbol vl_error_NoCandidateDevices; +extern const sd_varlink_symbol vl_error_ConflictingDiskLabelPresent; +extern const sd_varlink_symbol vl_error_InsufficientFreeSpace; +extern const sd_varlink_symbol vl_error_DiskTooSmall; diff --git a/src/shared/varlink-io.systemd.SysInstall.c b/src/shared/varlink-io.systemd.SysInstall.c new file mode 100644 index 0000000000000..c93a0276fae06 --- /dev/null +++ b/src/shared/varlink-io.systemd.SysInstall.c @@ -0,0 +1,125 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-varlink-idl.h" + +#include "varlink-io.systemd.SysInstall.h" +#include "varlink-io.systemd.Repart.h" + +static SD_VARLINK_DEFINE_ENUM_TYPE( + ProgressPhase, + + SD_VARLINK_DEFINE_ENUM_VALUE(encrypt_credentials), + SD_VARLINK_DEFINE_ENUM_VALUE(install_partitions), + SD_VARLINK_DEFINE_ENUM_VALUE(mount_partitions), + SD_VARLINK_DEFINE_ENUM_VALUE(install_kernel), + SD_VARLINK_DEFINE_ENUM_VALUE(install_bootloader), + SD_VARLINK_DEFINE_ENUM_VALUE(unmount_partitions)); + +static SD_VARLINK_DEFINE_ENUM_TYPE( + DeviceFit, + + SD_VARLINK_DEFINE_ENUM_VALUE(enough_free_space), + SD_VARLINK_DEFINE_ENUM_VALUE(insufficent_free_space), + SD_VARLINK_DEFINE_ENUM_VALUE(disk_too_small), + SD_VARLINK_DEFINE_ENUM_VALUE(conflicting_disk_label_present)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + Credential, + SD_VARLINK_FIELD_COMMENT("The id of the credential."), + SD_VARLINK_DEFINE_FIELD(id, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("The value of the credential in base64 encoding."), + SD_VARLINK_DEFINE_FIELD(value, SD_VARLINK_STRING, 0)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + Run, + SD_VARLINK_SUPPORTS_MORE, + SD_VARLINK_FIELD_COMMENT("Full path to the block device node to operate on."), + SD_VARLINK_DEFINE_INPUT(node, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Path to directory containing definition files."), + SD_VARLINK_DEFINE_INPUT(definitions, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If true, fully erase the target block device."), + SD_VARLINK_DEFINE_INPUT(erase, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("If true, EFI variables are modified to register the installed boot loader in the firmware's boot options database."), + SD_VARLINK_DEFINE_INPUT(variables, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The path to a kernel image, if missing the current kernel is dedected and used."), + SD_VARLINK_DEFINE_INPUT(kernelImagePath, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + + SD_VARLINK_FIELD_COMMENT("If true, the current locale is copied to target system"), + SD_VARLINK_DEFINE_INPUT(copyLocale, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If true, the current keymap is copied to target system"), + SD_VARLINK_DEFINE_INPUT(copyKeymap, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If true, the current timezone is copied to target system"), + SD_VARLINK_DEFINE_INPUT(copyTimezone, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + + SD_VARLINK_FIELD_COMMENT("A list of credentials to be installed to target system."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(credentials, Credential, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + + SD_VARLINK_FIELD_COMMENT("If used with the 'more' flag, a phase identifier is sent in progress updates."), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(phase, ProgressPhase, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If used with the 'more' flag, an object identifier string is sent in progress updates."), + SD_VARLINK_DEFINE_OUTPUT(object, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If used with the 'more' flag, a progress percentage (specific to the work done for the specified phase+object is sent in progress updates)."), + SD_VARLINK_DEFINE_OUTPUT(progress, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + ListCandidateDevices, + SD_VARLINK_REQUIRES_MORE, + SD_VARLINK_FIELD_COMMENT("Path to directory containing definition files, used to evaluate the fit of the target OS for each block device."), + SD_VARLINK_DEFINE_INPUT(definitions, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If true, keep the call open after the initial enumeration and stream live add/remove notifications as block-subsystem uevents arrive. The end of the initial enumeration is marked by exactly one notification with action='ready' and no other fields. Defaults to false."), + SD_VARLINK_DEFINE_INPUT(subscribe, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Discriminator field. Only set in subscribe mode. 'add' carries the full device record, 'remove' carries only node, 'ready' carries no other fields and is sent once after the initial enumeration."), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(action, BlockDeviceAction, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The device node path of the block device."), + SD_VARLINK_DEFINE_OUTPUT(node, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("List of symlinks pointing to the device node, if any."), + SD_VARLINK_DEFINE_OUTPUT(symlinks, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The Linux kernel disk sequence number identifying the medium."), + SD_VARLINK_DEFINE_OUTPUT(diskseq, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The size of the block device in bytes."), + SD_VARLINK_DEFINE_OUTPUT(sizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The device vendor string if known."), + SD_VARLINK_DEFINE_OUTPUT(vendor, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The device model string if known."), + SD_VARLINK_DEFINE_OUTPUT(model, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The subsystem the block device belongs to if known."), + SD_VARLINK_DEFINE_OUTPUT(subsystem, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The fit indicating whether the OS would have enough available space on the device."), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(fit, DeviceFit, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Minimal size of the disk required for the installation."), + SD_VARLINK_DEFINE_OUTPUT(minimalSizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Additional free space needed for the installation."), + SD_VARLINK_DEFINE_OUTPUT(needFreeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Current allocated size of this block device."), + SD_VARLINK_DEFINE_OUTPUT(currentSizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_SysInstall, + "io.systemd.SysInstall", + SD_VARLINK_INTERFACE_COMMENT("API for installing the OS to another block device."), + + SD_VARLINK_SYMBOL_COMMENT("Progress phase identifiers. Note that we might add more phases here, and thus identifiers. Frontends can choose to display the phase to the user in some human readable form, or not do that, but if they do it and they receive a notification for a so far unknown phase, they should just ignore it."), + &vl_type_ProgressPhase, + + SD_VARLINK_SYMBOL_COMMENT("Description about the fit of an OS on a block device."), + &vl_type_DeviceFit, + + SD_VARLINK_SYMBOL_COMMENT("Discriminator on streamed ListCandidateDevices replies in subscribe mode."), + &vl_type_BlockDeviceAction, + + SD_VARLINK_SYMBOL_COMMENT("A credential to install to target OS."), + &vl_type_Credential, + + SD_VARLINK_SYMBOL_COMMENT("Invoke the actual installation of the OS. If invoked with 'more' enabled will report progress, otherwise will just report completion."), + &vl_method_Run, + SD_VARLINK_SYMBOL_COMMENT("An incompatible disk label present, and not told to erase it."), + &vl_error_ConflictingDiskLabelPresent, + SD_VARLINK_SYMBOL_COMMENT("The target disk has insufficient free space to fit all requested partitions. (But the disk would fit, if emptied.)"), + &vl_error_InsufficientFreeSpace, + SD_VARLINK_SYMBOL_COMMENT("The target disk is too small to fit the installation. (Regardless if emptied or not.)"), + &vl_error_DiskTooSmall, + + SD_VARLINK_SYMBOL_COMMENT("Return a list of candidate block devices, i.e. that support partition scanning and other requirements for successful operation."), + &vl_method_ListCandidateDevices, + SD_VARLINK_SYMBOL_COMMENT("Not a single candidate block device could be found."), + &vl_error_NoCandidateDevices); diff --git a/src/shared/varlink-io.systemd.SysInstall.h b/src/shared/varlink-io.systemd.SysInstall.h new file mode 100644 index 0000000000000..d412e07b03c47 --- /dev/null +++ b/src/shared/varlink-io.systemd.SysInstall.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink-idl.h" + +extern const sd_varlink_interface vl_interface_io_systemd_SysInstall; diff --git a/src/sysinstall/io.systemd.sysinstall.policy b/src/sysinstall/io.systemd.sysinstall.policy new file mode 100644 index 0000000000000..d6bc796e52117 --- /dev/null +++ b/src/sysinstall/io.systemd.sysinstall.policy @@ -0,0 +1,40 @@ + + + + + + + + The systemd Project + https://systemd.io + + + Install OS to disk + Authentication is required to install an OS to disk. + + auth_admin + auth_admin + auth_admin_keep + + + + + List candidate devices + Authentication is required to list device candidates. + + auth_admin + auth_admin + auth_admin_keep + + + diff --git a/src/sysinstall/meson.build b/src/sysinstall/meson.build index 1d8be6036564b..79618d69338a5 100644 --- a/src/sysinstall/meson.build +++ b/src/sysinstall/meson.build @@ -8,3 +8,8 @@ executables += [ 'sources' : files('sysinstall.c'), }, ] + +if conf.get('ENABLE_SYSINSTALL') == 1 + install_data('io.systemd.sysinstall.policy', + install_dir : polkitpolicydir) +endif diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c index a9aee0535f925..7560e34a0b52a 100644 --- a/src/sysinstall/sysinstall.c +++ b/src/sysinstall/sysinstall.c @@ -11,6 +11,7 @@ #include "blockdev-list.h" #include "build.h" #include "build-path.h" +#include "bus-polkit.h" #include "chase.h" #include "conf-files.h" #include "constants.h" @@ -24,6 +25,7 @@ #include "format-util.h" #include "fs-util.h" #include "glyph-util.h" +#include "hashmap.h" #include "help-util.h" #include "image-policy.h" #include "json-util.h" @@ -39,8 +41,10 @@ #include "parse-util.h" #include "path-util.h" #include "prompt-util.h" +#include "string-table.h" #include "strv.h" #include "terminal-util.h" +#include "varlink-io.systemd.SysInstall.h" #include "varlink-util.h" static char *arg_node = NULL; @@ -58,12 +62,98 @@ static bool arg_copy_keymap = true; static bool arg_copy_timezone = true; static bool arg_chrome = true; static bool arg_mute_console = false; +static bool arg_varlink = false; STATIC_DESTRUCTOR_REGISTER(arg_node, freep); STATIC_DESTRUCTOR_REGISTER(arg_definitions, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_kernel_image, freep); STATIC_DESTRUCTOR_REGISTER(arg_credentials, machine_credential_context_done); +typedef enum ProgressPhase { + PROGRESS_ENCRYPT_CREDENTIALS, + PROGRESS_INSTALL_PARTITIONS, + PROGRESS_MOUNT_PARTITIONS, + PROGRESS_INSTALL_KERNEL, + PROGRESS_INSTALL_BOOTLOADER, + PROGRESS_UNMOUNT_PARTITIONS, + _PROGRESS_PHASE_MAX, + _PROGRESS_PHASE_INVALID = -EINVAL, +} ProgressPhase; + +static const char *progress_phase_table[_PROGRESS_PHASE_MAX] = { + [PROGRESS_ENCRYPT_CREDENTIALS] = "encrypt-credentials", + [PROGRESS_INSTALL_PARTITIONS] = "install-partitions", + [PROGRESS_MOUNT_PARTITIONS] = "mount-partitions", + [PROGRESS_INSTALL_KERNEL] = "install-kernel", + [PROGRESS_INSTALL_BOOTLOADER] = "install-bootloader", + [PROGRESS_UNMOUNT_PARTITIONS] = "unmount-partitions", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(progress_phase, ProgressPhase); + +static const char *progress_phase_log_table[_PROGRESS_PHASE_MAX] = { + [PROGRESS_ENCRYPT_CREDENTIALS] = "Encrypting credentials...", + [PROGRESS_INSTALL_PARTITIONS] = "Installing partitions...", + [PROGRESS_MOUNT_PARTITIONS] = "Mounting partitions...", + [PROGRESS_INSTALL_KERNEL] = "Installing kernel...", + [PROGRESS_INSTALL_BOOTLOADER] = "Installing boot loader...", + [PROGRESS_UNMOUNT_PARTITIONS] = "Unmounting partitions...", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(progress_phase_log, ProgressPhase); + +typedef enum DeviceFit { + DEVICE_FIT_ENOUGH_FREE_SPACE, + DEVICE_FIT_INSUFFICIENT_FREE_SPACE, + DEVICE_FIT_DISK_TOO_SMALL, + DEVICE_FIT_CONFLICTING_DISK_LABEL_PRESENT, + _DEVICE_FIT_MAX, + _DEVICE_FIT_INVALID = -EINVAL, +} DeviceFit; + +static const char *device_fit_table[_DEVICE_FIT_MAX] = { + [DEVICE_FIT_ENOUGH_FREE_SPACE] = "enough-free-space", + [DEVICE_FIT_INSUFFICIENT_FREE_SPACE] = "insufficient-free-space", + [DEVICE_FIT_DISK_TOO_SMALL] = "disk-too-small", + [DEVICE_FIT_CONFLICTING_DISK_LABEL_PRESENT] = "conflicting-disk-label-present", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(device_fit, DeviceFit); + +typedef struct SysInstallContext { + bool copy_locale; + bool copy_keymap; + bool copy_timezone; + MachineCredentialContext credentials; + char **definitions; + bool erase; + char *node; + char *kernel_filename; + int kernel_fd; + bool touch_variables; + + sd_varlink *repart_link; + + sd_varlink *link; /* If 'more' is used on the Varlink call, we'll send progress info over this link */ +} SysInstallContext; + +static void sysinstall_context_done(SysInstallContext *c) { + assert(c); + + strv_free(c->definitions); + + free(c->node); + + free(c->kernel_filename); + safe_close(c->kernel_fd); + + machine_credential_context_done(&c->credentials); + + sd_varlink_flush_close_unref(c->repart_link); + + sd_varlink_unref(c->link); +} + static int help(void) { int r; @@ -210,6 +300,12 @@ static int parse_argv(int argc, char *argv[]) { return log_oom(); } + r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + if (r > 0) + arg_varlink = true; + return 1; } @@ -403,6 +499,195 @@ static int prompt_block_device(sd_varlink **repart_link, char **ret_node) { return 0; } +static int sysinstall_context_notify( + SysInstallContext *context, + ProgressPhase phase, + const char *object, + unsigned percent) { + + int r; + + assert(context); + assert(phase >= 0); + assert(phase < _PROGRESS_PHASE_MAX); + + log_notice("%s%s%s", + emoji_enabled() ? phase == PROGRESS_ENCRYPT_CREDENTIALS ? glyph(GLYPH_LOCK_AND_KEY) : glyph(GLYPH_COMPUTER_DISK) : "", + emoji_enabled() ? " " : "", + progress_phase_log_to_string(phase)); + + if (context->link) { + r = sd_varlink_notifybo( + context->link, + JSON_BUILD_PAIR_ENUM("phase", progress_phase_to_string(phase)), + JSON_BUILD_PAIR_STRING_NON_EMPTY("object", object), + JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("progress", percent, UINT_MAX)); + if (r < 0) + log_debug_errno(r, "Failed to send varlink notify progress notification, ignoring: %m"); + + r = sd_varlink_flush(context->link); + if (r < 0) + log_debug_errno(r, "Failed to flush varlink notify progress notification, ignoring: %m"); + } + + return 0; +} + +typedef struct RepartResult { + int ret; + SysInstallContext *context; +} RepartResult; + +static int handle_repart_reply( + sd_varlink *link, + sd_json_variant *reply, + const char *error_id, + sd_varlink_reply_flags_t flags, + void *userdata) { + + RepartResult *result = userdata; + int r; + + assert(result); + + struct { + uint64_t min_size; + uint64_t current_size; + uint64_t need_free; + + const char *phase; + const char *object; + unsigned progress; + } p = { + .min_size = UINT64_MAX, + .current_size = UINT64_MAX, + .need_free = UINT64_MAX, + .progress = UINT_MAX, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "minimalSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, min_size), 0 }, + { "currentSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, current_size), 0 }, + { "needFreeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, need_free), 0 }, + { "phase", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_const_string, voffsetof(p, phase), 0 }, + { "object", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_const_string, voffsetof(p, object), 0 }, + { "progress", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, progress), 0 }, + {} + }; + + r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); + if (r < 0) + return result->ret = r; + + if (error_id) { + const char *sysinstall_error_id = NULL; + + if (streq(error_id, "io.systemd.Repart.InsufficientFreeSpace")) { + sysinstall_error_id = "io.systemd.SysInstall.InsufficientFreeSpace"; + result->ret = log_error_errno(SYNTHETIC_ERRNO(ENOSPC), "Not enough free space on disk, cannot install."); + } else if (streq(error_id, "io.systemd.Repart.DiskTooSmall")) { + sysinstall_error_id = "io.systemd.SysInstall.DiskTooSmall"; + + result->ret = log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Disk too small for installation, cannot install."); + } else if (streq(error_id, "io.systemd.Repart.ConflictingDiskLabelPresent")) { + sysinstall_error_id = "io.systemd.SysInstall.ConflictingDiskLabelPresent"; + + result->ret = log_error_errno( + SYNTHETIC_ERRNO(EHWPOISON), + "A conflicting disk label is already present on the target disk, cannot install unless disk is erased."); + } + + if (sysinstall_error_id && result->context->link) { + r = sd_varlink_errorbo( + result->context->link, + sysinstall_error_id, + JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("currentSizeBytes", p.current_size, UINT64_MAX), + JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("needFreeBytes", p.need_free, UINT64_MAX), + JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("minimalSizeBytes", p.min_size, UINT64_MAX)); + + if (r < 0) + return result->ret = r; + + return result->ret; + } + + r = sd_varlink_error_to_errno(error_id, reply); /* If this is a system errno style error, output it with %m */ + if (r != -EBADR) + return result->ret = log_error_errno(r, "Failed to issue io.systemd.Repart.Run() varlink call: %m"); + + return result->ret = log_error_errno(r, "Failed to issue io.systemd.Repart.Run() varlink call: %s", error_id); + } + + if ((p.progress != UINT_MAX || p.object) && result->context->link) + (void) sysinstall_context_notify(result->context, PROGRESS_INSTALL_PARTITIONS, p.object, p.progress); + + return result->ret = 0; +} + +static int sysinstall_context_invoke_repart_run(SysInstallContext *context) { + + int r; + + assert(context); + + r = connect_to_repart(&context->repart_link); + if (r < 0) + return r; + + /* Seeding the partitions might be very slow, disable timeout */ + r = sd_varlink_set_relative_timeout(context->repart_link, UINT64_MAX); + if (r < 0) + return log_error_errno(r, "Failed to disable IPC timeout: %m"); + + RepartResult result = { + .context = context, + }; + + sd_varlink_set_userdata(context->repart_link, &result); + + r = sd_varlink_bind_reply(context->repart_link, handle_repart_reply); + if (r < 0) + return log_error_errno(r, "Failed to bind repart reply callback: %m"); + + r = sd_varlink_observebo( + context->repart_link, + "io.systemd.Repart.Run", + SD_JSON_BUILD_PAIR_STRING("node", context->node), + SD_JSON_BUILD_PAIR_STRING("empty", context->erase ? "force" : "allow"), + SD_JSON_BUILD_PAIR_BOOLEAN("dryRun", false), + SD_JSON_BUILD_PAIR_CONDITION(!!context->definitions, "definitions", SD_JSON_BUILD_STRV(context->definitions)), + SD_JSON_BUILD_PAIR_BOOLEAN("deferPartitionsEmpty", true), + SD_JSON_BUILD_PAIR_BOOLEAN("deferPartitionsFactoryReset", true)); + if (r < 0) + return log_error_errno(r, "Failed to issue io.systemd.Repart.Run() varlink call: %m"); + + for (;;) { + r = sd_varlink_is_idle(context->repart_link); + if (r < 0) + return log_error_errno(r, "Failed to check if varlink connection is idle: %m"); + if (r > 0) + break; + + r = sd_varlink_process(context->repart_link); + if (r < 0) + return log_error_errno(r, "Failed to process varlink connection: %m"); + if (r != 0) + continue; + + r = sd_varlink_wait(context->repart_link, USEC_INFINITY); + if (r < 0) + return log_error_errno(r, "Failed to wait for varlink connection events: %m"); + } + + sd_varlink_set_userdata(context->repart_link, NULL); + + r = sd_varlink_bind_reply(context->repart_link, NULL); + if (r < 0) + return log_error_errno(r, "Failed to unbind repart reply callback: %m"); + + return result.ret; +} + static int read_space_metrics( sd_json_variant *v, uint64_t *min_size, @@ -447,6 +732,7 @@ static int invoke_repart( const char *node, bool erase, bool dry_run, + char **definitions, uint64_t *min_size, /* initialized both on success and error */ uint64_t *current_size, /* ditto */ uint64_t *need_free) { /* ditto */ @@ -481,7 +767,7 @@ static int invoke_repart( SD_JSON_BUILD_PAIR_STRING("node", node), SD_JSON_BUILD_PAIR_STRING("empty", erase ? "force" : "allow"), SD_JSON_BUILD_PAIR_BOOLEAN("dryRun", dry_run), - SD_JSON_BUILD_PAIR_CONDITION(!!arg_definitions, "definitions", SD_JSON_BUILD_STRV(arg_definitions)), + SD_JSON_BUILD_PAIR_CONDITION(!!definitions, "definitions", SD_JSON_BUILD_STRV(definitions)), SD_JSON_BUILD_PAIR_BOOLEAN("deferPartitionsEmpty", true), SD_JSON_BUILD_PAIR_BOOLEAN("deferPartitionsFactoryReset", true)); if (r < 0) { @@ -656,6 +942,7 @@ static int validate_run(sd_varlink **repart_link, const char *node) { node, try_erase, /* dry_run= */ true, + arg_definitions, &min_size, ¤t_size, &need_free); @@ -705,12 +992,9 @@ static int validate_run(sd_varlink **repart_link, const char *node) { } } -static int show_summary(void) { +static int sysinstall_context_show_summary(SysInstallContext *context) { int r; - if (!arg_summary) - return 0; - printf("\n" "%sSummary:%s\n", ansi_underline(), ansi_normal()); @@ -721,12 +1005,12 @@ static int show_summary(void) { r = table_add_many( table, TABLE_FIELD, "Selected Disk", - TABLE_STRING, arg_node, + TABLE_STRING, context->node, TABLE_FIELD, "Erase Disk", - TABLE_BOOLEAN, arg_erase, - TABLE_SET_COLOR, arg_erase ? ansi_highlight_red() : NULL, + TABLE_BOOLEAN, context->erase, + TABLE_SET_COLOR, context->erase ? ansi_highlight_red() : NULL, TABLE_FIELD, "Register in Firmware", - TABLE_BOOLEAN, arg_touch_variables); + TABLE_BOOLEAN, context->touch_variables); if (r < 0) return table_log_add_error(r); @@ -739,7 +1023,7 @@ static int show_summary(void) { }; STRV_FOREACH_PAIR(id, text, map) { - MachineCredential *c = machine_credential_find(&arg_credentials, *id); + MachineCredential *c = machine_credential_find(&context->credentials, *id); if (!c) continue; @@ -756,7 +1040,7 @@ static int show_summary(void) { } unsigned n_extra_credentials = 0; - FOREACH_ARRAY(cred, arg_credentials.credentials, arg_credentials.n_credentials) { + FOREACH_ARRAY(cred, context->credentials.credentials, context->credentials.n_credentials) { bool covered = false; STRV_FOREACH_PAIR(id, text, map) @@ -894,6 +1178,7 @@ static int connect_to_bootctl(sd_varlink **link) { static int invoke_bootctl_install( sd_varlink **link, + bool variables, const char *root_dir, int root_fd) { int r; @@ -919,7 +1204,7 @@ static int invoke_bootctl_install( SD_JSON_BUILD_PAIR_STRING("operation", "new"), SD_JSON_BUILD_PAIR_INTEGER("rootFileDescriptor", fd_idx), SD_JSON_BUILD_PAIR_STRING("rootDirectory", root_dir), - SD_JSON_BUILD_PAIR_BOOLEAN("touchVariables", arg_touch_variables)); + SD_JSON_BUILD_PAIR_BOOLEAN("touchVariables", variables)); if (r < 0) return r; @@ -1017,14 +1302,11 @@ static int maybe_reboot(void) { return 0; } -static int read_credential_locale(void) { +static int read_credential_locale(MachineCredentialContext *credentials) { int r; - if (!arg_copy_locale) - return 0; - - if (machine_credential_find(&arg_credentials, "firstboot.locale") || - machine_credential_find(&arg_credentials, "firstboot.locale-messages")) + if (machine_credential_find(credentials, "firstboot.locale") || + machine_credential_find(credentials, "firstboot.locale-messages")) return 0; /* For the main locale we check the two env vars, and if neither is there, we use LC_NUMERIC, since @@ -1032,14 +1314,14 @@ static int read_credential_locale(void) { * separate setting after all */ const char *l = getenv("LC_ALL") ?: getenv("LANG") ?: setlocale(LC_NUMERIC, NULL); if (l) { - r = machine_credential_add(&arg_credentials, "firstboot.locale", l, /* size= */ SIZE_MAX); + r = machine_credential_add(credentials, "firstboot.locale", l, /* size= */ SIZE_MAX); if (r < 0) return log_oom(); } const char *m = setlocale(LC_MESSAGES, NULL); if (m && !streq_ptr(m, l)) { - r = machine_credential_add(&arg_credentials, "firstboot.locale-messages", m, /* size= */ SIZE_MAX); + r = machine_credential_add(credentials, "firstboot.locale-messages", m, /* size= */ SIZE_MAX); if (r < 0) return log_oom(); } @@ -1047,13 +1329,10 @@ static int read_credential_locale(void) { return 0; } -static int read_credential_keymap(void) { +static int read_credential_keymap(MachineCredentialContext *credentials) { int r; - if (!arg_copy_keymap) - return 0; - - if (machine_credential_find(&arg_credentials, "firstboot.keymap")) + if (machine_credential_find(credentials, "firstboot.keymap")) return 0; _cleanup_free_ char *keymap = NULL; @@ -1065,7 +1344,7 @@ static int read_credential_keymap(void) { return log_error_errno(r, "Failed to parse '%s': %m", etc_vconsole_conf()); if (!isempty(keymap)) { - r = machine_credential_add(&arg_credentials, "firstboot.keymap", keymap, /* size= */ SIZE_MAX); + r = machine_credential_add(credentials, "firstboot.keymap", keymap, /* size= */ SIZE_MAX); if (r < 0) return log_oom(); } @@ -1073,13 +1352,10 @@ static int read_credential_keymap(void) { return 0; } -static int read_credential_timezone(void) { +static int read_credential_timezone(MachineCredentialContext *credentials) { int r; - if (!arg_copy_timezone) - return 0; - - if (machine_credential_find(&arg_credentials, "firstboot.timezone")) + if (machine_credential_find(credentials, "firstboot.timezone")) return 0; _cleanup_free_ char *tz = NULL; @@ -1087,7 +1363,7 @@ static int read_credential_timezone(void) { if (r < 0) log_warning_errno(r, "Failed to read timezone, skipping timezone propagation: %m"); else { - r = machine_credential_add(&arg_credentials, "firstboot.timezone", tz, /* size= */ SIZE_MAX); + r = machine_credential_add(credentials, "firstboot.timezone", tz, /* size= */ SIZE_MAX); if (r < 0) return log_oom(); } @@ -1095,20 +1371,26 @@ static int read_credential_timezone(void) { return 0; } -static int read_credentials(void) { +static int sysinstall_context_read_credentials(SysInstallContext *context) { int r; - r = read_credential_locale(); - if (r < 0) - return r; + if (context->copy_locale) { + r = read_credential_locale(&context->credentials); + if (r < 0) + return r; + } - r = read_credential_keymap(); - if (r < 0) - return r; + if (context->copy_keymap) { + r = read_credential_keymap(&context->credentials); + if (r < 0) + return r; + } - r = read_credential_timezone(); - if (r < 0) - return r; + if (context->copy_timezone) { + r = read_credential_timezone(&context->credentials); + if (r < 0) + return r; + } return 0; } @@ -1179,13 +1461,13 @@ static int encrypt_one_credential(sd_varlink **link, const MachineCredential *in return 0; } -static int encrypt_credentials(sd_varlink **link, char ***encrypted) { +static int encrypt_credentials(sd_varlink **link, MachineCredentialContext *credentials, char ***encrypted) { int r; assert(link); assert(encrypted); - FOREACH_ARRAY(cred, arg_credentials.credentials, arg_credentials.n_credentials) { + FOREACH_ARRAY(cred, credentials->credentials, credentials->n_credentials) { r = encrypt_one_credential(link, cred, encrypted); if (r < 0) return r; @@ -1206,11 +1488,22 @@ static const ImagePolicy image_policy = { .default_flags = PARTITION_POLICY_IGNORE, }; -static int settle_definitions(void) { +static int settle_definitions(char **definitions, char ***ret_definitions) { + + _cleanup_strv_free_ char **d = NULL; int r; - if (arg_definitions) + assert(ret_definitions); + + if (definitions) { + d = strv_copy(definitions); + if (!d) + return log_oom(); + + *ret_definitions = TAKE_PTR(d); + return 0; + } /* If /usr/lib/repart.sysinstall.d/ is populated, use it, otherwise use the regular definition * files */ @@ -1226,11 +1519,549 @@ static int settle_definitions(void) { return log_error_errno(r, "Failed to enumerate *.conf files: %m"); if (!strv_isempty(files)) { - arg_definitions = strv_copy(CONF_PATHS_STRV("repart.sysinstall.d")); - if (!arg_definitions) + d = strv_copy(CONF_PATHS_STRV("repart.sysinstall.d")); + if (!d) return log_oom(); + + *ret_definitions = TAKE_PTR(d); + } + + return 0; +} + +static int sysinstall_context_settle_definitions(SysInstallContext *context, + char **definitions) { + + return settle_definitions(definitions, &context->definitions); +} + +static int sysinstall_context_settle_kernel_image(SysInstallContext *context, + const char *kernel_image) { + + _cleanup_free_ char *kernel_filename = NULL; + _cleanup_close_ int kernel_fd = -EBADF; + int r; + + assert(context->kernel_fd < 0); + assert(!context->kernel_filename); + + if (kernel_image) { + r = path_extract_filename(kernel_image, &kernel_filename); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", kernel_image); + if (r == O_DIRECTORY) + return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Kernel path '%s' refers to directory, must be regular file, refusing.", kernel_image); + + kernel_fd = xopenat_full(XAT_FDROOT, kernel_image, O_RDONLY|O_CLOEXEC, XO_REGULAR, MODE_INVALID); + if (kernel_fd < 0) + return log_error_errno(kernel_fd, "Failed to open kernel image '%s': %m", kernel_image); + + } else { + r = find_current_kernel(&kernel_filename, &kernel_fd); + if (r < 0) + return r; } + context->kernel_filename = TAKE_PTR(kernel_filename); + context->kernel_fd = TAKE_FD(kernel_fd); + + return 0; +} + +static int sysinstall_context_run(SysInstallContext *context) { + + int r; + + assert(context); + + (void) sysinstall_context_notify(context, PROGRESS_ENCRYPT_CREDENTIALS, NULL, UINT_MAX); + + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *creds_link = NULL; + _cleanup_strv_free_ char **encrypted_credentials = NULL; + r = encrypt_credentials(&creds_link, &context->credentials, &encrypted_credentials); + if (r < 0) + return r; + + (void) sysinstall_context_notify(context, PROGRESS_INSTALL_PARTITIONS, NULL, UINT_MAX); + + /* Do the main part of the installation */ + + r = sysinstall_context_invoke_repart_run(context); + if (r < 0) + return r; + + (void) sysinstall_context_notify(context, PROGRESS_MOUNT_PARTITIONS, NULL, UINT_MAX); + + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_freep) char *root_dir = NULL; + _cleanup_close_ int root_fd = -EBADF; + r = mount_image_privately_interactively( + context->node, + &image_policy, + DISSECT_IMAGE_REQUIRE_ROOT | + DISSECT_IMAGE_RELAX_VAR_CHECK | + DISSECT_IMAGE_ALLOW_USERSPACE_VERITY | + DISSECT_IMAGE_DISCARD_ANY | + DISSECT_IMAGE_GPT_ONLY | + DISSECT_IMAGE_FSCK | + DISSECT_IMAGE_USR_NO_ROOT | + DISSECT_IMAGE_ADD_PARTITION_DEVICES | + DISSECT_IMAGE_PIN_PARTITION_DEVICES, + &root_dir, + &root_fd, + &loop_device); + if (r < 0) + return log_error_errno(r, "Failed to mount new image: %m"); + + (void) sysinstall_context_notify(context, PROGRESS_INSTALL_KERNEL, NULL, UINT_MAX); + + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *bootctl_link = NULL; + r = invoke_bootctl_link(&bootctl_link, root_dir, root_fd, context->kernel_filename, context->kernel_fd, encrypted_credentials); + if (r < 0) + return r; + + (void) sysinstall_context_notify(context, PROGRESS_INSTALL_BOOTLOADER, NULL, UINT_MAX); + + r = invoke_bootctl_install(&bootctl_link, context->touch_variables, root_dir, root_fd); + if (r < 0) + return r; + + (void) sysinstall_context_notify(context, PROGRESS_UNMOUNT_PARTITIONS, NULL, UINT_MAX); + + root_fd = safe_close(root_fd); + r = umount_recursive(root_dir, /* flags= */ 0); + if (r < 0) + log_warning_errno(r, "Failed to unmount target disk, proceeding anyway: %m"); + loop_device = loop_device_unref(loop_device); + sync(); + + return 0; +} + +typedef struct ListCandidateDevicesContext { + char **definitions; + bool subscribe; + + sd_varlink *repart_link; /* A repart connection to get candidate devices */ + sd_varlink *dry_run_repart_link; /* A second repart connection to perform a dry run on each node */ + + sd_varlink *link; +} ListCandidateDevicesContext; + +static ListCandidateDevicesContext* list_candidate_devices_context_new(void) { + ListCandidateDevicesContext *context = new(ListCandidateDevicesContext, 1); + + if (!context) + return NULL; + + *context = (ListCandidateDevicesContext) {}; + + return context; +} + +static ListCandidateDevicesContext* list_candidate_devices_context_free(ListCandidateDevicesContext *context) { + if (!context) + return NULL; + + strv_free(context->definitions); + + context->repart_link = sd_varlink_flush_close_unref(context->repart_link); + context->dry_run_repart_link = sd_varlink_flush_close_unref(context->dry_run_repart_link); + context->link = sd_varlink_unref(context->link); + + return mfree(context); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(ListCandidateDevicesContext*, list_candidate_devices_context_free); + +static int list_candidate_devices_context_settle_definitions(ListCandidateDevicesContext *context, + char **definitions) { + + return settle_definitions(definitions, &context->definitions); +} + +static void vl_on_disconnect(sd_varlink_server *server, sd_varlink *link, void *userdata) { + assert(server); + assert(link); + + list_candidate_devices_context_free(sd_varlink_set_userdata(link, NULL)); +} + +typedef struct DevicesResponse { + const char *node; + char **symlinks; + uint64_t diskseq; + uint64_t size; + const char *model; + const char *vendor; + const char *subsystem; + const char *action; +} DevicesResponse; + +static void devices_response_done(DevicesResponse *p) { + assert(p); + + p->symlinks = strv_free(p->symlinks); +} + +static int fetch_candidate_devices_reply( + sd_varlink *repart_link, + sd_json_variant *reply, + const char *error_id, + sd_varlink_reply_flags_t flags, + void *userdata) { + + int r; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + ListCandidateDevicesContext *context = ASSERT_PTR(userdata); + + if (error_id) { + if (streq(error_id, "io.systemd.Repart.NoCandidateDevices")) + return sd_varlink_error(context->link, "io.systemd.SysInstall.NoCandidateDevices", NULL); + + return sd_varlink_error(context->link, error_id, NULL); + } + + static const sd_json_dispatch_field dispatch_table[] = { + { "node", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(DevicesResponse, node), 0 }, + { "symlinks", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(DevicesResponse, symlinks), 0 }, + { "diskseq", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(DevicesResponse, diskseq), 0 }, + { "sizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(DevicesResponse, size), 0 }, + { "model", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(DevicesResponse, model), 0 }, + { "vendor", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(DevicesResponse, vendor), 0 }, + { "subsystem", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(DevicesResponse, subsystem), 0 }, + { "action", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(DevicesResponse, action), 0 }, + {} + }; + + _cleanup_(devices_response_done) DevicesResponse p = { + .diskseq = UINT64_MAX, + .size = UINT64_MAX, + }; + r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); + if (r < 0) + return r; + + if (context->subscribe) { + /* The action needs to be ready, remove or add else we don't support the action */ + if (streq(p.action, "ready")) + return sd_varlink_notifybo(context->link, SD_JSON_BUILD_PAIR("action", JSON_BUILD_CONST_STRING("ready"))); + + if (streq(p.action, "remove")) + return sd_varlink_notifybo(context->link, + SD_JSON_BUILD_PAIR("action", JSON_BUILD_CONST_STRING("remove")), + SD_JSON_BUILD_PAIR_STRING("node", p.node)); + + if (!streq(p.action, "add")) { + log_debug("Skip unsupported action '%s' while fetching candidate devices.", p.action); + return 0; + } + } + + uint64_t min_size = UINT64_MAX, current_size = UINT64_MAX, need_free = UINT64_MAX; + r = invoke_repart( + &context->dry_run_repart_link, + p.node, + /* erase= */ false, + /* dry_run= */ true, + context->definitions, + &min_size, + ¤t_size, + &need_free); + + DeviceFit fit; + if (r < 0) { + if (r == -ENOSPC) + fit = DEVICE_FIT_INSUFFICIENT_FREE_SPACE; + else if (r == -E2BIG) + fit = DEVICE_FIT_DISK_TOO_SMALL; + else if (r == -EHWPOISON) + fit = DEVICE_FIT_CONFLICTING_DISK_LABEL_PRESENT; + else + return r; + } else + fit = DEVICE_FIT_ENOUGH_FREE_SPACE; + + r = sd_json_buildo(&v, + SD_JSON_BUILD_PAIR_STRING("node", p.node), + JSON_BUILD_PAIR_STRV_NON_EMPTY("symlinks", p.symlinks), + JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("diskseq", p.diskseq, UINT64_MAX), + JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("sizeBytes", p.size, UINT64_MAX), + JSON_BUILD_PAIR_STRING_NON_EMPTY("model", p.model), + JSON_BUILD_PAIR_STRING_NON_EMPTY("vendor", p.vendor), + JSON_BUILD_PAIR_STRING_NON_EMPTY("subsystem", p.subsystem), + JSON_BUILD_PAIR_ENUM("fit", device_fit_to_string(fit)), + JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("currentSizeBytes", current_size, UINT64_MAX), + JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("needFreeBytes", need_free, UINT64_MAX), + JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("minimalSizeBytes", min_size, UINT64_MAX), + SD_JSON_BUILD_PAIR_CONDITION(context->subscribe, "action", JSON_BUILD_CONST_STRING("add"))); + if (r < 0) + return r; + + if (FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES)) + return sd_varlink_notify(context->link, v); + + return sd_varlink_reply(context->link, v); +} + +typedef struct ListCandidateDevicesParameters { + char **definitions; + bool subscribe; +} ListCandidateDevicesParameters; + +static void list_candidate_devices_parameters_done(ListCandidateDevicesParameters *p) { + assert(p); + + p->definitions = strv_free(p->definitions); +} + +static int vl_method_list_candidate_devices( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + int r; + + assert(link); + + sd_varlink_server *varlink_server = sd_varlink_get_server(link); + sd_event *event = sd_varlink_server_get_event(varlink_server); + Hashmap **polkit_registry = ASSERT_PTR(sd_varlink_server_get_userdata(varlink_server)); + + assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)); + + r = varlink_verify_polkit_async( + link, + /* bus= */ NULL, + "io.systemd.sysinstall.ListCandidateDevices", + /* details= */ NULL, + polkit_registry); + if (r <= 0) + return r; + + static const sd_json_dispatch_field dispatch_table[] = { + { "definitions", SD_JSON_VARIANT_ARRAY, json_dispatch_strv_path, offsetof(ListCandidateDevicesParameters, definitions), SD_JSON_STRICT }, + { "subscribe", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(ListCandidateDevicesParameters, subscribe), 0 }, + {} + }; + + _cleanup_(list_candidate_devices_parameters_done) ListCandidateDevicesParameters p = {}; + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + _cleanup_(list_candidate_devices_context_freep) ListCandidateDevicesContext* context = list_candidate_devices_context_new(); + if (!context) + return log_oom(); + + context->subscribe = p.subscribe; + r = list_candidate_devices_context_settle_definitions(context, p.definitions); + if (r < 0) + return r; + + context->link = sd_varlink_ref(link); + + r = connect_to_repart(&context->repart_link); + if (r < 0) + return r; + + r = sd_varlink_attach_event(context->repart_link, event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno( + r, + "Failed to attach io.systemd.Repart.ListCandidateDevices() varlink connection to event loop: %m"); + + r = sd_varlink_bind_reply(context->repart_link, fetch_candidate_devices_reply); + if (r < 0) + return r; + + r = sd_varlink_observebo( + context->repart_link, + "io.systemd.Repart.ListCandidateDevices", + SD_JSON_BUILD_PAIR_BOOLEAN("subscribe", context->subscribe), + SD_JSON_BUILD_PAIR_BOOLEAN("ignoreRoot", true)); + + if (r < 0) + return log_error_errno( + r, + "Failed to issue io.systemd.Repart.ListCandidateDevices() varlink call: %m"); + + r = sd_varlink_server_bind_disconnect(varlink_server, vl_on_disconnect); + if (r < 0) + return r; + + /* The context is freed in vl_on_disconnect() */ + sd_varlink_set_userdata(context->repart_link, context); + sd_varlink_set_userdata(link, TAKE_PTR(context)); + + return 0; +} + +typedef struct RunParameters { + char *node; + char **definitions; + bool erase; + bool variables; + char *kernel_image; + bool copy_locale; + bool copy_keymap; + bool copy_timezone; + sd_json_variant *credentials; +} RunParameters; + +static void run_parameters_done(RunParameters *p) { + assert(p); + + p->node = mfree(p->node); + p->definitions = strv_free(p->definitions); + p->kernel_image = mfree(p->kernel_image); + p->credentials = sd_json_variant_unref(p->credentials); +} + +typedef struct CredentialParameters { + const char *id; + struct iovec value; +} CredentialParameters; + +static void credential_parameters_done(CredentialParameters *p) { + assert(p); + + iovec_done_erase(&p->value); +} + +static int credentials_from_json_array(MachineCredentialContext *credentials, sd_json_variant *v) { + + int r; + sd_json_variant *credential; + + assert(credentials); + + static const sd_json_dispatch_field dispatch_table[] = { + { "id", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CredentialParameters, id), SD_JSON_MANDATORY }, + { "value", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(CredentialParameters, value), SD_JSON_MANDATORY }, + {} + }; + + JSON_VARIANT_ARRAY_FOREACH(credential, v) { + _cleanup_(credential_parameters_done) CredentialParameters p = {}; + + r = sd_json_dispatch(credential, dispatch_table, /* flags= */ 0, &p); + if (r < 0) + return r; + + r = machine_credential_add(credentials, p.id, p.value.iov_base, p.value.iov_len); + if (r < 0) + return r; + } + + return 0; +} + +static int vl_method_run( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { + { "node", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(RunParameters, node), SD_JSON_MANDATORY | SD_JSON_STRICT }, + { "definitions", SD_JSON_VARIANT_ARRAY, json_dispatch_strv_path, offsetof(RunParameters, definitions), SD_JSON_NULLABLE | SD_JSON_STRICT }, + { "erase", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(RunParameters, erase), SD_JSON_MANDATORY }, + { "variables", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(RunParameters, variables), SD_JSON_NULLABLE }, + { "kernelImagePath", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(RunParameters, kernel_image), SD_JSON_NULLABLE | SD_JSON_STRICT }, + { "copyLocale", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(RunParameters, copy_locale), SD_JSON_NULLABLE }, + { "copyKeymap", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(RunParameters, copy_keymap), SD_JSON_NULLABLE }, + { "copyTimezone", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(RunParameters, copy_timezone), SD_JSON_NULLABLE }, + { "credentials", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant, offsetof(RunParameters, credentials), SD_JSON_NULLABLE }, + {} + }; + + int r; + + assert(link); + + sd_varlink_server *varlink_server = sd_varlink_get_server(link); + Hashmap **polkit_registry = ASSERT_PTR(sd_varlink_server_get_userdata(varlink_server)); + + r = varlink_verify_polkit_async( + link, + /* bus= */ NULL, + "io.systemd.sysinstall.Run", + /* details= */ NULL, + polkit_registry); + if (r <= 0) + return r; + + _cleanup_(run_parameters_done) RunParameters p = {}; + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + _cleanup_(sysinstall_context_done) SysInstallContext context = (SysInstallContext) { + .copy_locale = p.copy_locale, + .copy_keymap = p.copy_keymap, + .copy_timezone = p.copy_timezone, + .erase = p.erase, + .touch_variables = p.variables, + .node = TAKE_PTR(p.node), + .kernel_fd = -EBADF, + }; + + r = sysinstall_context_settle_definitions(&context, p.definitions); + if (r < 0) + return r; + + if (FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + context.link = sd_varlink_ref(link); + + r = credentials_from_json_array(&context.credentials, p.credentials); + if (r < 0) + return r; + + r = sysinstall_context_read_credentials(&context); + if (r < 0) + return r; + + r = sysinstall_context_settle_kernel_image(&context, p.kernel_image); + if (r < 0) + return r; + + r = sysinstall_context_run(&context); + if (r < 0) + return r; + + return sd_varlink_reply(link, NULL); +} + +static int vl_server(void) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; + _cleanup_hashmap_free_ Hashmap *polkit_registry = NULL; + int r; + + /* Invocation as Varlink service */ + + r = varlink_server_new( + &varlink_server, + 0, + /* userdata= */ &polkit_registry); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_SysInstall); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = sd_varlink_server_bind_method_many( + varlink_server, + "io.systemd.SysInstall.ListCandidateDevices", vl_method_list_candidate_devices, + "io.systemd.SysInstall.Run", vl_method_run); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink methods: %m"); + + r = sd_varlink_server_loop_auto(varlink_server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + return 0; } @@ -1244,8 +2075,6 @@ static void end_marker(void) { } static int run(int argc, char *argv[]) { - _cleanup_free_ char *kernel_filename = NULL; - _cleanup_close_ int kernel_fd = -EBADF; int r; setlocale(LC_ALL, ""); @@ -1256,7 +2085,18 @@ static int run(int argc, char *argv[]) { log_setup(); - r = settle_definitions(); + if (arg_varlink) + return vl_server(); + + _cleanup_(sysinstall_context_done) SysInstallContext context = (SysInstallContext) { + .copy_locale = arg_copy_locale, + .copy_keymap = arg_copy_keymap, + .copy_timezone = arg_copy_timezone, + .credentials = TAKE_STRUCT(arg_credentials), + .kernel_fd = -EBADF, + }; + + r = sysinstall_context_settle_definitions(&context, arg_definitions); if (r < 0) return r; @@ -1291,6 +2131,7 @@ static int run(int argc, char *argv[]) { /* node= */ NULL, /* erase= */ true, /* dry_run= */ true, + arg_definitions, &min_size, /* current_size= */ NULL, /* need_free= */ NULL); @@ -1324,34 +2165,28 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - r = read_credentials(); + r = sysinstall_context_read_credentials(&context); if (r < 0) return r; - if (arg_kernel_image) { - r = path_extract_filename(arg_kernel_image, &kernel_filename); - if (r < 0) - return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", arg_kernel_image); - if (r == O_DIRECTORY) - return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Kernel path '%s' refers to directory, must be regular file, refusing.", arg_kernel_image); - - kernel_fd = xopenat_full(XAT_FDROOT, arg_kernel_image, O_RDONLY|O_CLOEXEC, XO_REGULAR, MODE_INVALID); - if (kernel_fd < 0) - return log_error_errno(kernel_fd, "Failed to open kernel image '%s': %m", arg_kernel_image); - } else { - r = find_current_kernel(&kernel_filename, &kernel_fd); - if (r < 0) - return r; - } + r = sysinstall_context_settle_kernel_image(&context, arg_kernel_image); + if (r < 0) + return r; /* Verify we have everything we need */ assert(arg_node); assert(arg_erase >= 0); assert(arg_touch_variables >= 0); - r = show_summary(); - if (r < 0) - return r; + context.node = TAKE_PTR(arg_node); + context.touch_variables = arg_touch_variables; + context.erase = arg_erase; + + if (arg_summary) { + r = sysinstall_context_show_summary(&context); + if (r < 0) + return r; + } r = prompt_confirm(); if (r < 0) @@ -1359,79 +2194,10 @@ static int run(int argc, char *argv[]) { putchar('\n'); - log_notice("%s%sEncrypting credentials...", - emoji_enabled() ? glyph(GLYPH_LOCK_AND_KEY) : "", emoji_enabled() ? " " : ""); - - _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *creds_link = NULL; - _cleanup_strv_free_ char **encrypted_credentials = NULL; - r = encrypt_credentials(&creds_link, &encrypted_credentials); - if (r < 0) - return r; - - log_notice("%s%sInstalling partitions...", - emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); - - /* Do the main part of the installation */ - r = invoke_repart( - &repart_link, - arg_node, - arg_erase, - /* dry_run= */ false, - /* min_size= */ NULL, - /* current_size= */ NULL, - /* need_free= */ NULL); - if (r < 0) - return r; - - log_notice("%s%sMounting partitions...", - emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); - - _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; - _cleanup_(umount_and_freep) char *root_dir = NULL; - _cleanup_close_ int root_fd = -EBADF; - r = mount_image_privately_interactively( - arg_node, - &image_policy, - DISSECT_IMAGE_REQUIRE_ROOT | - DISSECT_IMAGE_RELAX_VAR_CHECK | - DISSECT_IMAGE_ALLOW_USERSPACE_VERITY | - DISSECT_IMAGE_DISCARD_ANY | - DISSECT_IMAGE_GPT_ONLY | - DISSECT_IMAGE_FSCK | - DISSECT_IMAGE_USR_NO_ROOT | - DISSECT_IMAGE_ADD_PARTITION_DEVICES | - DISSECT_IMAGE_PIN_PARTITION_DEVICES, - &root_dir, - &root_fd, - &loop_device); - if (r < 0) - return log_error_errno(r, "Failed to mount new image: %m"); - - log_notice("%s%sInstalling kernel...", - emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); - - _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *bootctl_link = NULL; - r = invoke_bootctl_link(&bootctl_link, root_dir, root_fd, kernel_filename, kernel_fd, encrypted_credentials); + r = sysinstall_context_run(&context); if (r < 0) return r; - log_notice("%s%sInstalling boot loader...", - emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); - - r = invoke_bootctl_install(&bootctl_link, root_dir, root_fd); - if (r < 0) - return r; - - log_notice("%s%sUnmounting partitions...", - emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); - - root_fd = safe_close(root_fd); - r = umount_recursive(root_dir, /* flags= */ 0); - if (r < 0) - log_warning_errno(r, "Failed to unmount target disk, proceeding anyway: %m"); - loop_device = loop_device_unref(loop_device); - sync(); - log_notice("%s%sInstallation succeeded.", emoji_enabled() ? glyph(GLYPH_SPARKLES) : "", emoji_enabled() ? " " : ""); diff --git a/units/meson.build b/units/meson.build index 569aa2e2a2cf0..615ddd80ae7af 100644 --- a/units/meson.build +++ b/units/meson.build @@ -840,6 +840,15 @@ units = [ 'conditions' : ['ENABLE_SYSINSTALL'], 'symlinks' : ['system-install.target.wants/'], }, + { + 'file' : 'systemd-sysinstall.socket', + 'conditions' : ['ENABLE_SYSINSTALL'], + 'symlinks' : ['sockets.target.wants/'], + }, + { + 'file' : 'systemd-sysinstall@.service', + 'conditions' : ['ENABLE_SYSINSTALL'], + }, { 'file' : 'systemd-sysupdate-reboot.service', 'conditions' : ['ENABLE_SYSUPDATE'], diff --git a/units/systemd-sysinstall.service b/units/systemd-sysinstall.service index a330db2ef3054..6b25fed13d146 100644 --- a/units/systemd-sysinstall.service +++ b/units/systemd-sysinstall.service @@ -14,6 +14,8 @@ Wants=systemd-logind.service After=systemd-logind.service [Service] +Type=oneshot +RemainAfterExit=yes ExecStart=systemd-sysinstall --variables=yes --reboot=yes --mute-console=yes StandardOutput=tty StandardInput=tty diff --git a/units/systemd-sysinstall.socket b/units/systemd-sysinstall.socket new file mode 100644 index 0000000000000..5eb5f86b6d1fe --- /dev/null +++ b/units/systemd-sysinstall.socket @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=System Install Tool +Documentation=man:systemd-sysinstall(8) +DefaultDependencies=no +Before=sockets.target +Conflicts=shutdown.target +Before=shutdown.target + +[Socket] +ListenStream=/run/systemd/io.systemd.SysInstall +Symlinks=/run/varlink/registry/io.systemd.SysInstall +FileDescriptorName=varlink +SocketMode=0666 +Accept=yes +MaxConnectionsPerSource=16 +XAttrEntryPoint=user.varlink=entrypoint +XAttrListen=user.varlink=listen +XAttrAccept=user.varlink=server diff --git a/units/systemd-sysinstall@.service b/units/systemd-sysinstall@.service new file mode 100644 index 0000000000000..54cb72c128f52 --- /dev/null +++ b/units/systemd-sysinstall@.service @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=System Install Tool +Documentation=man:systemd-sysinstall(8) +DefaultDependencies=no +Wants=modprobe@loop.service modprobe@dm_mod.service +After=modprobe@loop.service modprobe@dm_mod.service systemd-tpm2-setup-early.service +Conflicts=shutdown.target +Before=shutdown.target + +[Service] +ExecStart=systemd-sysinstall From 2acdbae8eb334b3b898f31f5c63acd9f5f6d7fa1 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Fri, 26 Jun 2026 18:48:42 +0200 Subject: [PATCH 38/71] test: Add basic test for sysinstall varlink interface This adds a basic test for the newly added varlink interface of sysinstall. --- test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh | 260 ++++++++++-------- 1 file changed, 151 insertions(+), 109 deletions(-) diff --git a/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh b/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh index d4ca6e0a0d163..7c51e89ebbd35 100755 --- a/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh +++ b/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh @@ -34,12 +34,15 @@ if systemd-detect-virt -cq; then exit 0 fi +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + # shellcheck source=test/units/util.sh . "$(dirname "$0")"/util.sh -WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)" -LOOPDEV="" -MOUNTED=0 + +CRED_VALUE="systemd-sysinstall test credential payload" +CRED_VALUE_BASE64=$(echo -n "$CRED_VALUE" | base64 -w0) cleanup() { set +e @@ -53,42 +56,42 @@ cleanup() { fi rm -rf "$WORKDIR" } -trap cleanup EXIT -# 1) Build a small fake "OS source" tree. systemd-sysinstall picks this up via -# the repart.sysinstall.d definitions: CopyFiles= seeds the new root -# partition with these files. -SOURCE_ROOT="$WORKDIR/sourceroot" -mkdir -p "$SOURCE_ROOT/usr/lib" "$SOURCE_ROOT/etc" +create_fake_os_source_tree() { + # 1) Build a small fake "OS source" tree. systemd-sysinstall picks this up via + # the repart.sysinstall.d definitions: CopyFiles= seeds the new root + # partition with these files. + SOURCE_ROOT="$WORKDIR/sourceroot" + mkdir -p "$SOURCE_ROOT/usr/lib" "$SOURCE_ROOT/etc" -cat >"$SOURCE_ROOT/usr/lib/os-release" <<'EOF' + cat >"$SOURCE_ROOT/usr/lib/os-release" <<'EOF' ID=testos NAME="Test OS" PRETTY_NAME="Test OS for systemd-sysinstall" VERSION_ID=1 EOF -ln -s ../usr/lib/os-release "$SOURCE_ROOT/etc/os-release" - -# 2) Build a minimal UKI. bootctl link only requires a valid PE with .osrel and -# the systemd-stub SBAT marker, so the .linux/.initrd contents do not need -# to be a real kernel. -echo "fake-kernel" >"$WORKDIR/vmlinuz" -echo "fake-initrd" >"$WORKDIR/initrd" - -ukify build \ - --linux "$WORKDIR/vmlinuz" \ - --initrd "$WORKDIR/initrd" \ - --os-release "@$SOURCE_ROOT/usr/lib/os-release" \ - --uname "1.2.3-testkernel" \ - --cmdline "quiet" \ - --output "$WORKDIR/testuki.efi" - -# 3) Build a sysinstall partition definition: a single ESP plus a root -# partition seeded from the fake source tree. -DEFS="$WORKDIR/sysinstall.d" -mkdir -p "$DEFS" - -cat >"$DEFS/10-esp.conf" <"$WORKDIR/vmlinuz" + echo "fake-initrd" >"$WORKDIR/initrd" + + ukify build \ + --linux "$WORKDIR/vmlinuz" \ + --initrd "$WORKDIR/initrd" \ + --os-release "@$SOURCE_ROOT/usr/lib/os-release" \ + --uname "1.2.3-testkernel" \ + --cmdline "quiet" \ + --output "$WORKDIR/testuki.efi" + + # 3) Build a sysinstall partition definition: a single ESP plus a root + # partition seeded from the fake source tree. + DEFS="$WORKDIR/sysinstall.d" + mkdir -p "$DEFS" + + cat >"$DEFS/10-esp.conf" <"$DEFS/20-root.conf" <"$DEFS/20-root.conf" </dev/null + + # The UKI file referenced in the entry must exist on the ESP. + UKI_PATH=$(awk '/^uki / { print $2 }' "$ENTRY") + test -n "$UKI_PATH" + test -f "$ESP$UKI_PATH" + + # bootctl install should have placed sd-boot on the ESP. + find "$ESP/EFI/systemd" -type f -iname 'systemd-boot*.efi' | grep . >/dev/null + + # The credential we passed via --set-credential= must have been encrypted and + # placed next to the UKI, and must be referenced as 'extra' from the entry. + UKI_DIR="$(dirname "$ESP$UKI_PATH")" + TOKEN_DIR="$(basename "$UKI_DIR")" + test -s "$UKI_DIR/marker.cred" + grep -E "^extra /$TOKEN_DIR/marker\.cred$" "$ENTRY" >/dev/null + + # Locale/keymap/timezone propagation is off, so those .cred files must NOT + # exist on the ESP. + test ! -e "$UKI_DIR/firstboot.locale.cred" + test ! -e "$UKI_DIR/firstboot.keymap.cred" + test ! -e "$UKI_DIR/firstboot.timezone.cred" + + # 3) The seeded files from the fake source tree must end up in the new root. + test -f "$MNT/usr/lib/os-release" + grep '^ID=testos$' "$MNT/usr/lib/os-release" >/dev/null +} -# 5) Run the installer non-interactively against the target image. Also stash a -# literal credential ('marker') so we can verify it ends up next to the UKI -# and is referenced from the boot loader entry. -CRED_VALUE="systemd-sysinstall test credential payload" -systemd-sysinstall \ - --welcome=no \ - --chrome=no \ - --confirm=no \ - --summary=no \ - --erase=yes \ - --variables=no \ - --reboot=no \ - --mute-console=no \ - --copy-locale=no \ - --copy-keymap=no \ - --copy-timezone=no \ - --set-credential="marker:$CRED_VALUE" \ - --kernel="$WORKDIR/testuki.efi" \ - --definitions="$DEFS" \ - "$WORKDIR/target.img" - -# 6) Attach the freshly installed image as a loopback device for inspection. -LOOPDEV="$(systemd-dissect --attach "$WORKDIR/target.img")" - -# Verify the resulting on-disk layout. The disk must now carry a GPT with at -# least an ESP partition. -sfdisk_dump="$(sfdisk --dump "$LOOPDEV")" -assert_in "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" "$sfdisk_dump" - -# 7) Mount the image read-only and verify the installed artifacts: an entry -# file referencing the UKI on the ESP, the UKI itself, and the systemd-boot -# binary. -MNT="$WORKDIR/mnt" -mkdir -p "$MNT" - -systemd-dissect --mount --read-only "$LOOPDEV" "$MNT" -MOUNTED=1 - -ESP="$MNT/efi" -test -d "$ESP/loader/entries" - -# Exactly one entry should have been linked, and it should reference the UKI -# we passed via --kernel=. -ENTRY=$(find "$ESP/loader/entries" -maxdepth 1 -name '*.conf' -type f | head -n1) -test -n "$ENTRY" -grep -E "^uki /[^/]+/testuki\.efi$" "$ENTRY" >/dev/null - -# The UKI file referenced in the entry must exist on the ESP. -UKI_PATH=$(awk '/^uki / { print $2 }' "$ENTRY") -test -n "$UKI_PATH" -test -f "$ESP$UKI_PATH" - -# bootctl install should have placed sd-boot on the ESP. -find "$ESP/EFI/systemd" -type f -iname 'systemd-boot*.efi' | grep . >/dev/null - -# The credential we passed via --set-credential= must have been encrypted and -# placed next to the UKI, and must be referenced as 'extra' from the entry. -UKI_DIR="$(dirname "$ESP$UKI_PATH")" -TOKEN_DIR="$(basename "$UKI_DIR")" -test -s "$UKI_DIR/marker.cred" -grep -E "^extra /$TOKEN_DIR/marker\.cred$" "$ENTRY" >/dev/null - -# Locale/keymap/timezone propagation is off, so those .cred files must NOT -# exist on the ESP. -test ! -e "$UKI_DIR/firstboot.locale.cred" -test ! -e "$UKI_DIR/firstboot.keymap.cred" -test ! -e "$UKI_DIR/firstboot.timezone.cred" - -# 8) The seeded files from the fake source tree must end up in the new root. -test -f "$MNT/usr/lib/os-release" -grep '^ID=testos$' "$MNT/usr/lib/os-release" >/dev/null +testcase_sysinstall_basic() { + WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)" + LOOPDEV="" + MOUNTED=0 + + echo "WORKDIR=$WORKDIR" + + trap cleanup RETURN + + create_fake_os_source_tree + + # Run the installer non-interactively against the target image. Also stash a + # literal credential ('marker') so we can verify it ends up next to the UKI + # and is referenced from the boot loader entry. + systemd-sysinstall \ + --welcome=no \ + --chrome=no \ + --confirm=no \ + --summary=no \ + --erase=yes \ + --variables=no \ + --reboot=no \ + --mute-console=no \ + --copy-locale=no \ + --copy-keymap=no \ + --copy-timezone=no \ + --set-credential="marker:$CRED_VALUE" \ + --kernel="$WORKDIR/testuki.efi" \ + --definitions="$DEFS" \ + "$WORKDIR/target.img" + + validate_image + + cleanup +} + +testcase_sysinstall_varlink_basic() { + WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)" + LOOPDEV="" + MOUNTED=0 + + echo "WORKDIR=$WORKDIR" + + trap cleanup RETURN + + create_fake_os_source_tree + + # Run the installer via varlink against the target image. Also stash a + # literal credential ('marker') so we can verify it ends up next to the UKI + # and is referenced from the boot loader entry. + varlinkctl call /run/systemd/io.systemd.SysInstall io.systemd.SysInstall.Run "{\"erase\": true, \"variables\": false, \"credentials\" : [{ \"id\" : \"marker\", \"value\" : \"$CRED_VALUE_BASE64\" }], \"kernelImagePath\" : \"$WORKDIR/testuki.efi\", \"node\": \"$WORKDIR/target.img\", \"definitions\" : [\"$DEFS\"] }" --more + + validate_image +} + +run_testcases From 09859dbc9fd8644dbc2f45b5e74f6a0e48e98d41 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 10:01:32 +0900 Subject: [PATCH 39/71] tree-wide: drop unnecessary header inclusions --- .../cryptsetup-tokens/cryptsetup-token-systemd-fido2.c | 1 - .../cryptsetup-tokens/cryptsetup-token-systemd-pkcs11.c | 1 - .../cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c | 1 - src/cryptsetup/cryptsetup-tokens/luks2-fido2.c | 2 -- src/journal-remote/journal-gatewayd.c | 1 - src/journal-remote/journal-upload-journal.c | 2 -- src/journal-remote/journal-upload.h | 3 +-- src/shared/pkcs11-util.c | 4 ---- 8 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-fido2.c b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-fido2.c index 0bfe0c2ec7797..4cf6b7654c22a 100644 --- a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-fido2.c +++ b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-fido2.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include diff --git a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-pkcs11.c b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-pkcs11.c index 63be8a7c64a76..6952bb7ca2393 100644 --- a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-pkcs11.c +++ b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-pkcs11.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "sd-json.h" diff --git a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c index 552d81da1702d..450399ec129ab 100644 --- a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c +++ b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "alloc-util.h" diff --git a/src/cryptsetup/cryptsetup-tokens/luks2-fido2.c b/src/cryptsetup/cryptsetup-tokens/luks2-fido2.c index c6cfdcf6efeb8..eeb5ca0242308 100644 --- a/src/cryptsetup/cryptsetup-tokens/luks2-fido2.c +++ b/src/cryptsetup/cryptsetup-tokens/luks2-fido2.c @@ -1,7 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include - #include "alloc-util.h" #include "cryptsetup-token-util.h" #include "hexdecoct.h" diff --git a/src/journal-remote/journal-gatewayd.c b/src/journal-remote/journal-gatewayd.c index bb1f901d9c867..25465507208d9 100644 --- a/src/journal-remote/journal-gatewayd.c +++ b/src/journal-remote/journal-gatewayd.c @@ -1,7 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include -#include #include #include #include diff --git a/src/journal-remote/journal-upload-journal.c b/src/journal-remote/journal-upload-journal.c index 66cc4114f40e4..21974a67edc26 100644 --- a/src/journal-remote/journal-upload-journal.c +++ b/src/journal-remote/journal-upload-journal.c @@ -1,7 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include - #include "sd-daemon.h" #include "sd-event.h" #include "sd-journal.h" diff --git a/src/journal-remote/journal-upload.h b/src/journal-remote/journal-upload.h index 9ab0f535a4c06..3c857dc4b6579 100644 --- a/src/journal-remote/journal-upload.h +++ b/src/journal-remote/journal-upload.h @@ -2,8 +2,7 @@ #pragma once -#include - +#include "curl-util.h" #include "shared-forward.h" #include "journal-compression-util.h" diff --git a/src/shared/pkcs11-util.c b/src/shared/pkcs11-util.c index 60235fa3e0882..6002c2e0af73d 100644 --- a/src/shared/pkcs11-util.c +++ b/src/shared/pkcs11-util.c @@ -1,9 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#if HAVE_OPENSSL -# include -#endif - #include "sd-dlopen.h" #include "alloc-util.h" From 0f607da249992ad2efe63890b21b69af41bb42d2 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 20:28:34 +0900 Subject: [PATCH 40/71] pull: introduce pull-forward.h --- src/import/pull-common.h | 6 +----- src/import/pull-forward.h | 22 ++++++++++++++++++++++ src/import/pull-job.c | 1 - src/import/pull-job.h | 11 +---------- src/import/pull-oci.c | 1 + src/import/pull-oci.h | 6 +----- src/import/pull-raw.h | 6 +----- src/import/pull-tar.h | 6 +----- 8 files changed, 28 insertions(+), 31 deletions(-) create mode 100644 src/import/pull-forward.h diff --git a/src/import/pull-common.h b/src/import/pull-common.h index e4c0d365309d0..f38d4f1bae7d4 100644 --- a/src/import/pull-common.h +++ b/src/import/pull-common.h @@ -1,13 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once -#include "shared-forward.h" #include "import-common.h" #include "import-util.h" -#include "pull-job.h" - -typedef struct CurlGlue CurlGlue; -typedef struct PullJob PullJob; +#include "pull-forward.h" int pull_find_old_etags( const char *url, diff --git a/src/import/pull-forward.h b/src/import/pull-forward.h new file mode 100644 index 0000000000000..39ed1725ce784 --- /dev/null +++ b/src/import/pull-forward.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +typedef enum PullJobState PullJobState; + +typedef struct PullJob PullJob; + +typedef struct OciPull OciPull; +typedef struct RawPull RawPull; +typedef struct TarPull TarPull; + +typedef void (*PullJobFinished)(PullJob *job); +typedef int (*PullJobOpenDisk)(PullJob *job); +typedef int (*PullJobHeader)(PullJob *job, const char *header, size_t sz); +typedef void (*PullJobProgress)(PullJob *job); +typedef int (*PullJobNotFound)(PullJob *job, char **ret_new_url); + +typedef void (*OciPullFinished)(OciPull *pull, int error, void *userdata); +typedef void (*RawPullFinished)(RawPull *p, int error, void *userdata); +typedef void (*TarPullFinished)(TarPull *p, int error, void *userdata); diff --git a/src/import/pull-job.c b/src/import/pull-job.c index 7d83c994ff50a..119bb7528617d 100644 --- a/src/import/pull-job.c +++ b/src/import/pull-job.c @@ -15,7 +15,6 @@ #include "io-util.h" #include "log.h" #include "parse-util.h" -#include "pull-common.h" #include "pull-job.h" #include "string-util.h" #include "strv.h" diff --git a/src/import/pull-job.h b/src/import/pull-job.h index 00d001680ff20..2314cc2452916 100644 --- a/src/import/pull-job.h +++ b/src/import/pull-job.h @@ -1,19 +1,10 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once -#include #include #include -#include "shared-forward.h" - -typedef struct PullJob PullJob; - -typedef void (*PullJobFinished)(PullJob *job); -typedef int (*PullJobOpenDisk)(PullJob *job); -typedef int (*PullJobHeader)(PullJob *job, const char *header, size_t sz); -typedef void (*PullJobProgress)(PullJob *job); -typedef int (*PullJobNotFound)(PullJob *job, char **ret_new_url); +#include "pull-forward.h" typedef enum PullJobState { PULL_JOB_INIT, diff --git a/src/import/pull-oci.c b/src/import/pull-oci.c index b6fa71c902965..a9019f19a51c0 100644 --- a/src/import/pull-oci.c +++ b/src/import/pull-oci.c @@ -28,6 +28,7 @@ #include "pidref.h" #include "process-util.h" #include "pull-common.h" +#include "pull-job.h" #include "pull-oci.h" #include "rm-rf.h" #include "set.h" diff --git a/src/import/pull-oci.h b/src/import/pull-oci.h index 86a37c33ec210..4cf02b97e9189 100644 --- a/src/import/pull-oci.h +++ b/src/import/pull-oci.h @@ -1,12 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once -#include "shared-forward.h" #include "import-common.h" - -typedef struct OciPull OciPull; - -typedef void (*OciPullFinished)(OciPull *pull, int error, void *userdata); +#include "pull-forward.h" int oci_pull_new(OciPull **ret, sd_event *event, const char *image_root, OciPullFinished on_finished, void *userdata); OciPull* oci_pull_unref(OciPull *i); diff --git a/src/import/pull-raw.h b/src/import/pull-raw.h index d927d927f89aa..d28ec644ebefd 100644 --- a/src/import/pull-raw.h +++ b/src/import/pull-raw.h @@ -1,13 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once -#include "shared-forward.h" #include "import-common.h" #include "import-util.h" - -typedef struct RawPull RawPull; - -typedef void (*RawPullFinished)(RawPull *p, int error, void *userdata); +#include "pull-forward.h" int raw_pull_new(RawPull **ret, sd_event *event, const char *image_root, RawPullFinished on_finished, void *userdata); RawPull* raw_pull_unref(RawPull *p); diff --git a/src/import/pull-tar.h b/src/import/pull-tar.h index 39a9eacb35295..bcb998b82d08f 100644 --- a/src/import/pull-tar.h +++ b/src/import/pull-tar.h @@ -1,13 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once -#include "shared-forward.h" #include "import-common.h" #include "import-util.h" - -typedef struct TarPull TarPull; - -typedef void (*TarPullFinished)(TarPull *p, int error, void *userdata); +#include "pull-forward.h" int tar_pull_new(TarPull **ret, sd_event *event, const char *image_root, TarPullFinished on_finished, void *userdata); TarPull* tar_pull_unref(TarPull *p); From e51a0122b4829844ff1748970fb503ffd8d96c06 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 10:04:34 +0900 Subject: [PATCH 41/71] meson: use tpm2_cflags dependency rather than tpm2 --- src/pcrextend/meson.build | 2 +- src/pcrlock/meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pcrextend/meson.build b/src/pcrextend/meson.build index f2f5f3b46e3e8..ced1be5a90909 100644 --- a/src/pcrextend/meson.build +++ b/src/pcrextend/meson.build @@ -12,7 +12,7 @@ executables += [ 'sources' : files('pcrextend.c'), 'dependencies' : [ libopenssl_cflags, - tpm2, + tpm2_cflags, ], }, ] diff --git a/src/pcrlock/meson.build b/src/pcrlock/meson.build index dbe816402deca..d93a4f6069e62 100644 --- a/src/pcrlock/meson.build +++ b/src/pcrlock/meson.build @@ -13,7 +13,7 @@ executables += [ ), 'dependencies' : [ libopenssl_cflags, - tpm2, + tpm2_cflags, ], 'public' : true, }, From dc28de0eaab44590f2bceaa525261e8cfb907208 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 16:30:08 +0900 Subject: [PATCH 42/71] meson: do not pass space-separated list of libraries It was not clear that which version was returned by tpm2.version(). Let's explicitly check dependency one-by-one. --- meson.build | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/meson.build b/meson.build index a5572301db03b..8038ae61f3b4c 100644 --- a/meson.build +++ b/meson.build @@ -1239,11 +1239,20 @@ libfido2 = dependency('libfido2', libfido2_cflags = libfido2.partial_dependency(includes: true, compile_args: true) conf.set10('HAVE_LIBFIDO2', libfido2.found()) -tpm2 = dependency('tss2-esys tss2-rc tss2-mu tss2-tcti-device', - required : get_option('tpm2')) -tpm2_cflags = tpm2.partial_dependency(includes: true, compile_args: true) -conf.set10('HAVE_TPM2', tpm2.found()) -conf.set10('HAVE_TSS2_ESYS3', tpm2.found() and tpm2.version().version_compare('>= 3.0.0')) +tss2_esys = dependency('tss2-esys', required : get_option('tpm2')) +tss2_mu = dependency('tss2-mu', required : get_option('tpm2')) +tss2_rc = dependency('tss2-rc', required : get_option('tpm2')) +tss2_tcti_device = dependency('tss2-tcti-device', required : get_option('tpm2')) +tpm2_cflags = declare_dependency( + dependencies : [ + tss2_esys.partial_dependency(includes: true, compile_args: true), + tss2_mu.partial_dependency(includes: true, compile_args: true), + tss2_rc.partial_dependency(includes: true, compile_args: true), + tss2_tcti_device.partial_dependency(includes: true, compile_args: true), + ], +) +conf.set10('HAVE_TPM2', tss2_esys.found() and tss2_mu.found() and tss2_rc.found() and tss2_tcti_device.found()) +conf.set10('HAVE_TSS2_ESYS3', tss2_esys.found() and tss2_esys.version().version_compare('>= 3.0.0')) conf.set('TPM2_NVPCR_BASE', get_option('tpm2-nvpcr-base')) libdw = dependency('libdw', From cff424864d78f8f1cbb6b00ff5866e440e879bfc Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 11:12:43 +0900 Subject: [PATCH 43/71] meson: merge three glib cflags dependencies They are always used together, and only used by a unit test. Fine-graded cflags dependencies are not necessary. --- meson.build | 10 +++++++--- src/libsystemd/meson.build | 2 -- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/meson.build b/meson.build index 8038ae61f3b4c..14919bfce9f83 100644 --- a/meson.build +++ b/meson.build @@ -1353,9 +1353,13 @@ libgobject = dependency('gobject-2.0', libgio = dependency('gio-2.0', required : get_option('glib')) conf.set10('HAVE_GLIB', libglib.found() and libgobject.found() and libgio.found()) -libglib_cflags = libglib.partial_dependency(includes: true, compile_args: true) -libgobject_cflags = libgobject.partial_dependency(includes: true, compile_args: true) -libgio_cflags = libgio.partial_dependency(includes: true, compile_args: true) +libglib_cflags = declare_dependency( + dependencies : [ + libglib.partial_dependency(includes: true, compile_args: true), + libgobject.partial_dependency(includes: true, compile_args: true), + libgio.partial_dependency(includes: true, compile_args: true), + ], +) libdbus = dependency('dbus-1', version : '>= 1.3.2', diff --git a/src/libsystemd/meson.build b/src/libsystemd/meson.build index e393d7e86d860..cd3821bb8d6a5 100644 --- a/src/libsystemd/meson.build +++ b/src/libsystemd/meson.build @@ -226,9 +226,7 @@ libsystemd_tests += [ 'sources' : files('sd-bus/test-bus-marshal.c'), 'dependencies' : [ libdbus_cflags, - libgio_cflags, libglib_cflags, - libgobject_cflags, ], }, { From 4c5cbc970a976999d07664984599e6661f8ccd30 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 16:38:47 +0900 Subject: [PATCH 44/71] meson: merge two libelf-related clfags dependencies Both libraries are used only by elf-utils.c. We can merge them. --- meson.build | 8 ++++++-- src/shared/meson.build | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 14919bfce9f83..8f629df5b7ba2 100644 --- a/meson.build +++ b/meson.build @@ -1258,10 +1258,14 @@ conf.set('TPM2_NVPCR_BASE', get_option('tpm2-nvpcr-base')) libdw = dependency('libdw', version : '>=0.177', required : get_option('elfutils')) -libdw_cflags = libdw.partial_dependency(includes: true, compile_args: true) libelf = dependency('libelf', required : get_option('elfutils')) -libelf_cflags = libelf.partial_dependency(includes: true, compile_args: true) +libelf_cflags = declare_dependency( + dependencies : [ + libelf.partial_dependency(includes: true, compile_args: true), + libdw.partial_dependency(includes: true, compile_args: true), + ], +) conf.set10('HAVE_ELFUTILS', libdw.found() and libelf.found()) libz = dependency('zlib', diff --git a/src/shared/meson.build b/src/shared/meson.build index f9367ac2265f0..59511425c1d26 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -405,7 +405,6 @@ libshared_deps = [libacl_cflags, libcrypt_cflags, libcryptsetup_cflags, libcurl_cflags, - libdw_cflags, libelf_cflags, libfdisk_cflags, libfido2_cflags, From 5280cd7bdb9cf62935f9f3d652e577db3b96b1c4 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 03:52:36 +0900 Subject: [PATCH 45/71] tree-wide: check if necessary cflags dependencies are set This makes each `foo_cflags` dependency define a `SYSTEMD_CFLAGS_MARKER_FOO` macro, and checks if the macro is set when headers provided by external libraries are included. With this, we can fail fast at compile time if necessary `_cflags` dependencies are omitted in meson.build. Missing dependencies found by this mechanism have been added across the tree. --- meson.build | 199 +++++++++++++++---- src/analyze/meson.build | 5 +- src/basic/compress.c | 15 ++ src/bootctl/meson.build | 5 +- src/core/meson.build | 23 ++- src/coredump/meson.build | 5 +- src/creds/meson.build | 1 + src/cryptenroll/meson.build | 1 + src/cryptsetup/cryptsetup-tokens/meson.build | 5 +- src/cryptsetup/meson.build | 4 + src/dissect/meson.build | 3 + src/fuzz/meson.build | 8 +- src/growfs/meson.build | 3 + src/home/meson.build | 3 + src/imds/meson.build | 3 + src/import/meson.build | 12 +- src/journal-remote/meson.build | 5 +- src/journal/audit_type-to-name.awk | 3 + src/journal/meson.build | 7 + src/libsystemd/sd-bus/test-bus-marshal.c | 6 + src/locale/xkbcommon-util.h | 4 + src/login/meson.build | 3 + src/measure/meson.build | 5 +- src/network/meson.build | 8 +- src/nsresourced/meson.build | 3 + src/repart/meson.build | 4 + src/report/meson.build | 3 + src/sbsign/authenticode.h | 4 + src/shared/acl-util.h | 4 + src/shared/apparmor-util.h | 4 + src/shared/blkid-util.h | 3 + src/shared/bpf-link.h | 3 + src/shared/bpf-util.h | 3 + src/shared/crypto-util.h | 4 + src/shared/cryptsetup-util.h | 4 + src/shared/curl-util.h | 4 + src/shared/elf-util.c | 4 + src/shared/fdisk-util.h | 3 + src/shared/gnutls-util.h | 4 + src/shared/idn-util.h | 4 + src/shared/libarchive-util.h | 4 + src/shared/libaudit-util.h | 4 + src/shared/libcrypt-util.c | 3 + src/shared/libfido2-util.h | 4 + src/shared/libmount-util.h | 3 + src/shared/microhttpd-util.h | 4 + src/shared/module-util.h | 3 + src/shared/pam-util.h | 4 + src/shared/password-quality-util-passwdqc.c | 3 + src/shared/password-quality-util-pwquality.c | 3 + src/shared/pcre2-util.h | 3 + src/shared/pkcs11-util.h | 3 + src/shared/qrcode-util.c | 3 + src/shared/reboot-util.c | 4 + src/shared/seccomp-util.h | 4 + src/shared/selinux-util.h | 4 + src/shared/ssl-util.h | 4 + src/shared/tpm2-util.h | 4 + src/sysext/meson.build | 1 + src/systemctl/meson.build | 3 + src/test/gcrypt-util.h | 4 + src/test/meson.build | 55 ++++- src/tmpfiles/meson.build | 10 +- src/tpm2-setup/meson.build | 7 + src/udev/meson.build | 2 + src/veritysetup/meson.build | 5 +- 66 files changed, 476 insertions(+), 69 deletions(-) diff --git a/meson.build b/meson.build index 8f629df5b7ba2..1ce4c95368da3 100644 --- a/meson.build +++ b/meson.build @@ -1028,13 +1028,18 @@ endif if get_option('libc') == 'musl' libcrypt = [] - libcrypt_cflags = [] + libcrypt_cflags = declare_dependency( + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBCRYPT', + ) have = get_option('libcrypt').allowed() else libcrypt = dependency('libcrypt', 'libxcrypt', required : get_option('libcrypt'), version : '>=4.4.0') - libcrypt_cflags = libcrypt.partial_dependency(includes: true, compile_args: true) + libcrypt_cflags = declare_dependency( + dependencies : libcrypt.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBCRYPT', + ) have = libcrypt.found() endif conf.set10('HAVE_LIBCRYPT', have) @@ -1052,7 +1057,10 @@ bpf_compiler = get_option('bpf-compiler') libbpf = dependency('libbpf', required : bpf_framework, version : bpf_compiler == 'gcc' ? '>= 1.4.0' : '>= 0.1.0') -libbpf_cflags = libbpf.partial_dependency(includes: true, compile_args: true) +libbpf_cflags = declare_dependency( + dependencies : libbpf.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBBPF', +) conf.set10('HAVE_LIBBPF', libbpf.found()) libmount = dependency('mount', @@ -1060,12 +1068,18 @@ libmount = dependency('mount', required : get_option('libmount')) have = libmount.found() conf.set10('HAVE_LIBMOUNT', have) -libmount_cflags = libmount.partial_dependency(includes: true, compile_args: true) +libmount_cflags = declare_dependency( + dependencies : libmount.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBMOUNT', +) libfdisk = dependency('fdisk', version : '>= 2.35', required : get_option('fdisk')) -libfdisk_cflags = libfdisk.partial_dependency(includes: true, compile_args: true) +libfdisk_cflags = declare_dependency( + dependencies : libfdisk.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBFDISK', +) conf.set10('HAVE_LIBFDISK', libfdisk.found()) # This prefers pwquality if both are enabled or auto. @@ -1079,7 +1093,10 @@ if not have libpwquality = dependency('passwdqc', required : get_option('passwdqc')) endif -libpwquality_cflags = libpwquality.partial_dependency(includes: true, compile_args: true) +libpwquality_cflags = declare_dependency( + dependencies : libpwquality.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBPWQUALITY', +) conf.set10('HAVE_PWQUALITY', have) conf.set10('HAVE_PASSWDQC', not have and libpwquality.found()) @@ -1087,19 +1104,28 @@ libseccomp = dependency('libseccomp', version : '>= 2.4.0', required : get_option('seccomp')) conf.set10('HAVE_SECCOMP', libseccomp.found()) -libseccomp_cflags = libseccomp.partial_dependency(includes: true, compile_args: true) +libseccomp_cflags = declare_dependency( + dependencies : libseccomp.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBSECCOMP', +) libselinux = dependency('libselinux', version : '>= 2.1.9', required : get_option('selinux')) conf.set10('HAVE_SELINUX', libselinux.found()) -libselinux_cflags = libselinux.partial_dependency(includes: true, compile_args: true) +libselinux_cflags = declare_dependency( + dependencies : libselinux.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBSELINUX', +) libapparmor = dependency('libapparmor', version : '>= 2.13', required : get_option('apparmor')) conf.set10('HAVE_APPARMOR', libapparmor.found()) -libapparmor_cflags = libapparmor.partial_dependency(includes: true, compile_args: true) +libapparmor_cflags = declare_dependency( + dependencies : libapparmor.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBAPPARMOR', +) have = get_option('smack') and get_option('smack-run-label') != '' conf.set10('HAVE_SMACK_RUN_LABEL', have) @@ -1125,30 +1151,45 @@ conf.set10('ENABLE_POLKIT', install_polkit) libacl = dependency('libacl', required : get_option('acl')) conf.set10('HAVE_ACL', libacl.found()) -libacl_cflags = libacl.partial_dependency(includes: true, compile_args: true) +libacl_cflags = declare_dependency( + dependencies : libacl.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBACL', +) libaudit = dependency('audit', required : get_option('audit')) conf.set10('HAVE_AUDIT', libaudit.found()) -libaudit_cflags = libaudit.partial_dependency(includes: true, compile_args: true) +libaudit_cflags = declare_dependency( + dependencies : libaudit.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBAUDIT', +) libblkid = dependency('blkid', version : '>=2.37.0', required : get_option('blkid')) conf.set10('HAVE_BLKID', libblkid.found()) -libblkid_cflags = libblkid.partial_dependency(includes: true, compile_args: true) +libblkid_cflags = declare_dependency( + dependencies : libblkid.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBBLKID', +) libkmod = dependency('libkmod', version : '>= 15', required : get_option('kmod')) conf.set10('HAVE_KMOD', libkmod.found()) -libkmod_cflags = libkmod.partial_dependency(includes: true, compile_args: true) +libkmod_cflags = declare_dependency( + dependencies : libkmod.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBKMOD', +) libxenctrl = dependency('xencontrol', version : '>= 4.9', required : get_option('xenctrl')) conf.set10('HAVE_XENCTRL', libxenctrl.found()) -libxenctrl_cflags = libxenctrl.partial_dependency(includes: true, compile_args: true) +libxenctrl_cflags = declare_dependency( + dependencies : libxenctrl.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBXENCTRL', +) feature = get_option('pam') libpam = dependency('pam', @@ -1158,13 +1199,19 @@ if not libpam.found() libpam = cc.find_library('pam', required : feature) endif conf.set10('HAVE_PAM', libpam.found()) -libpam_cflags = libpam.partial_dependency(includes: true, compile_args: true) +libpam_cflags = declare_dependency( + dependencies : libpam.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBPAM', +) libmicrohttpd = dependency('libmicrohttpd', version : '>= 0.9.33', required : get_option('microhttpd')) conf.set10('HAVE_MICROHTTPD', libmicrohttpd.found()) -libmicrohttpd_cflags = libmicrohttpd.partial_dependency(includes: true, compile_args: true) +libmicrohttpd_cflags = declare_dependency( + dependencies : libmicrohttpd.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBMICROHTTPD', +) libcryptsetup = get_option('libcryptsetup') libcryptsetup_plugins = get_option('libcryptsetup-plugins') @@ -1178,7 +1225,10 @@ endif libcryptsetup = dependency('libcryptsetup', version : '>= 2.4.0', required : libcryptsetup) -libcryptsetup_cflags = libcryptsetup.partial_dependency(includes: true, compile_args: true) +libcryptsetup_cflags = declare_dependency( + dependencies : libcryptsetup.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBCRYPTSETUP', +) have = libcryptsetup.found() conf.set10('HAVE_LIBCRYPTSETUP', have) @@ -1188,19 +1238,28 @@ conf.set10('HAVE_LIBCRYPTSETUP_PLUGINS', libcurl = dependency('libcurl', version : '>= 7.32.0', required : get_option('libcurl')) -libcurl_cflags = libcurl.partial_dependency(includes: true, compile_args: true) +libcurl_cflags = declare_dependency( + dependencies : libcurl.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBCURL', +) conf.set10('HAVE_LIBCURL', libcurl.found()) conf.set10('CURL_NO_OLDIES', conf.get('BUILD_MODE_DEVELOPER') == 1) libidn2 = dependency('libidn2', required : get_option('libidn2')) -libidn2_cflags = libidn2.partial_dependency(includes: true, compile_args: true) +libidn2_cflags = declare_dependency( + dependencies : libidn2.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBIDN2', +) conf.set10('HAVE_LIBIDN2', libidn2.found()) libqrencode = dependency('libqrencode', version : '>= 3', required : get_option('qrencode')) -libqrencode_cflags = libqrencode.partial_dependency(includes: true, compile_args: true) +libqrencode_cflags = declare_dependency( + dependencies : libqrencode.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBQRENCODE', +) conf.set10('HAVE_QRENCODE', libqrencode.found()) feature = get_option('gcrypt') @@ -1208,7 +1267,10 @@ libgcrypt = dependency('libgcrypt', required : feature.disabled() ? feature : false) conf.set10('HAVE_GCRYPT', libgcrypt.found()) if libgcrypt.found() - libgcrypt_cflags = libgcrypt.partial_dependency(includes: true, compile_args: true) + libgcrypt_cflags = declare_dependency( + dependencies : libgcrypt.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBGCRYPT', + ) else libgcrypt_cflags = [] endif @@ -1217,26 +1279,38 @@ libgnutls = dependency('gnutls', version : '>= 3.1.4', required : get_option('gnutls')) conf.set10('HAVE_GNUTLS', libgnutls.found()) -libgnutls_cflags = libgnutls.partial_dependency(includes: true, compile_args: true) +libgnutls_cflags = declare_dependency( + dependencies : libgnutls.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBGNUTLS', +) libopenssl = dependency('openssl', version : '>= 3.0.0', required : get_option('openssl')) -libopenssl_cflags = libopenssl.partial_dependency(includes: true, compile_args: true) +libopenssl_cflags = declare_dependency( + dependencies : libopenssl.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBOPENSSL', +) conf.set10('HAVE_OPENSSL', libopenssl.found()) libp11kit = dependency('p11-kit-1', version : '>= 0.23.3', required : get_option('p11kit')) conf.set10('HAVE_P11KIT', libp11kit.found()) -libp11kit_cflags = libp11kit.partial_dependency(includes: true, compile_args: true) +libp11kit_cflags = declare_dependency( + dependencies : libp11kit.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBP11KIT', +) feature = get_option('libfido2').require( conf.get('HAVE_OPENSSL') == 1, error_message : 'openssl required') libfido2 = dependency('libfido2', required : feature) -libfido2_cflags = libfido2.partial_dependency(includes: true, compile_args: true) +libfido2_cflags = declare_dependency( + dependencies : libfido2.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBFIDO2', +) conf.set10('HAVE_LIBFIDO2', libfido2.found()) tss2_esys = dependency('tss2-esys', required : get_option('tpm2')) @@ -1250,6 +1324,7 @@ tpm2_cflags = declare_dependency( tss2_rc.partial_dependency(includes: true, compile_args: true), tss2_tcti_device.partial_dependency(includes: true, compile_args: true), ], + compile_args : '-DSYSTEMD_CFLAGS_MARKER_TPM2', ) conf.set10('HAVE_TPM2', tss2_esys.found() and tss2_mu.found() and tss2_rc.found() and tss2_tcti_device.found()) conf.set10('HAVE_TSS2_ESYS3', tss2_esys.found() and tss2_esys.version().version_compare('>= 3.0.0')) @@ -1265,13 +1340,17 @@ libelf_cflags = declare_dependency( libelf.partial_dependency(includes: true, compile_args: true), libdw.partial_dependency(includes: true, compile_args: true), ], + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBELF', ) conf.set10('HAVE_ELFUTILS', libdw.found() and libelf.found()) libz = dependency('zlib', required : get_option('zlib')) conf.set10('HAVE_ZLIB', libz.found()) -libz_cflags = libz.partial_dependency(includes: true, compile_args: true) +libz_cflags = declare_dependency( + dependencies : libz.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBZ', +) feature = get_option('bzip2') libbzip2 = dependency('bzip2', @@ -1281,24 +1360,36 @@ if not libbzip2.found() libbzip2 = cc.find_library('bz2', required : feature) endif conf.set10('HAVE_BZIP2', libbzip2.found()) -libbzip2_cflags = libbzip2.partial_dependency(includes: true, compile_args: true) +libbzip2_cflags = declare_dependency( + dependencies : libbzip2.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBBZIP2', +) libxz = dependency('liblzma', required : get_option('xz')) conf.set10('HAVE_XZ', libxz.found()) -libxz_cflags = libxz.partial_dependency(includes: true, compile_args: true) +libxz_cflags = declare_dependency( + dependencies : libxz.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBXZ', +) liblz4 = dependency('liblz4', version : '>= 1.3.0', required : get_option('lz4')) conf.set10('HAVE_LZ4', liblz4.found()) -liblz4_cflags = liblz4.partial_dependency(includes: true, compile_args: true) +liblz4_cflags = declare_dependency( + dependencies : liblz4.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBLZ4', +) libzstd = dependency('libzstd', version : '>= 1.4.0', required : get_option('zstd')) conf.set10('HAVE_ZSTD', libzstd.found()) -libzstd_cflags = libzstd.partial_dependency(includes: true, compile_args: true) +libzstd_cflags = declare_dependency( + dependencies : libzstd.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBZSTD', +) conf.set10('HAVE_COMPRESSION', libxz.found() or liblz4.found() or libzstd.found()) @@ -1334,18 +1425,27 @@ conf.set('DEFAULT_COMPRESSION', 'COMPRESSION_@0@'.format(compression.to_upper()) libarchive = dependency('libarchive', version : '>= 3.0', required : get_option('libarchive')) -libarchive_cflags = libarchive.partial_dependency(includes: true, compile_args: true) +libarchive_cflags = declare_dependency( + dependencies : libarchive.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBARCHIVE', +) conf.set10('HAVE_LIBARCHIVE', libarchive.found()) libxkbcommon = dependency('xkbcommon', version : '>= 0.3.0', required : get_option('xkbcommon')) -libxkbcommon_cflags = libxkbcommon.partial_dependency(includes: true, compile_args: true) +libxkbcommon_cflags = declare_dependency( + dependencies : libxkbcommon.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBXKBCOMMON', +) conf.set10('HAVE_XKBCOMMON', libxkbcommon.found()) libpcre2 = dependency('libpcre2-8', required : get_option('pcre2')) -libpcre2_cflags = libpcre2.partial_dependency(includes: true, compile_args: true) +libpcre2_cflags = declare_dependency( + dependencies : libpcre2.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBPCRE2', +) conf.set10('HAVE_PCRE2', libpcre2.found()) libglib = dependency('glib-2.0', @@ -1363,13 +1463,17 @@ libglib_cflags = declare_dependency( libgobject.partial_dependency(includes: true, compile_args: true), libgio.partial_dependency(includes: true, compile_args: true), ], + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBGLIB', ) libdbus = dependency('dbus-1', version : '>= 1.3.2', required : get_option('dbus')) conf.set10('HAVE_DBUS', libdbus.found()) -libdbus_cflags = libdbus.partial_dependency(includes: true, compile_args: true) +libdbus_cflags = declare_dependency( + dependencies : libdbus.partial_dependency(includes: true, compile_args: true), + compile_args : '-DSYSTEMD_CFLAGS_MARKER_LIBDBUS', +) dbusdatadir = libdbus.get_variable(pkgconfig: 'datadir', default_value: datadir) / 'dbus-1' @@ -1828,12 +1932,16 @@ if static_libsystemd != 'false' install_tag: 'libsystemd', install_dir : libdir, pic : static_libsystemd_pic, - dependencies : [liblz4_cflags, - libm, - libucontext, - libxz_cflags, - libzstd_cflags, - userspace], + dependencies : [ + libbzip2_cflags, + liblz4_cflags, + libm, + libucontext, + libxz_cflags, + libz_cflags, + libzstd_cflags, + userspace, + ], c_args : libsystemd_c_args + (static_libsystemd_pic ? [] : ['-fno-PIC'])) alias_target('libsystemd', libsystemd, install_libsystemd_static) @@ -1871,8 +1979,15 @@ if static_libudev != 'false' install_tag: 'libudev', install_dir : libdir, link_depends : libudev_sym, - dependencies : [libshared_deps, - userspace], + dependencies : [ + libbzip2_cflags, + liblz4_cflags, + libshared_deps, + libxz_cflags, + libz_cflags, + libzstd_cflags, + userspace, + ], c_args : static_libudev_pic ? [] : ['-fno-PIC'], pic : static_libudev_pic) diff --git a/src/analyze/meson.build b/src/analyze/meson.build index 67001845ac181..a8232eb851cd2 100644 --- a/src/analyze/meson.build +++ b/src/analyze/meson.build @@ -55,7 +55,10 @@ executables += [ libcore, libshared, ], - 'dependencies' : libseccomp_cflags, + 'dependencies' : [ + libseccomp_cflags, + tpm2_cflags, + ], 'install' : conf.get('ENABLE_ANALYZE') == 1, }, core_test_template + { diff --git a/src/basic/compress.c b/src/basic/compress.c index d75065781820f..70d0b7ced319c 100644 --- a/src/basic/compress.c +++ b/src/basic/compress.c @@ -6,25 +6,40 @@ #include #if HAVE_XZ +#ifndef SYSTEMD_CFLAGS_MARKER_LIBXZ +# error "missing libxz_cflags in meson dependency." +#endif #include #endif #if HAVE_LZ4 +#ifndef SYSTEMD_CFLAGS_MARKER_LIBLZ4 +# error "missing liblz4_cflags in meson dependency." +#endif #include #include #include #endif #if HAVE_ZSTD +#ifndef SYSTEMD_CFLAGS_MARKER_LIBZSTD +# error "missing libzstd_cflags in meson dependency." +#endif #include #include #endif #if HAVE_ZLIB +#ifndef SYSTEMD_CFLAGS_MARKER_LIBZ +# error "missing libz_cflags in meson dependency." +#endif #include #endif #if HAVE_BZIP2 +#ifndef SYSTEMD_CFLAGS_MARKER_LIBBZIP2 +# error "missing libbzip2_cflags in meson dependency." +#endif #include #endif diff --git a/src/bootctl/meson.build b/src/bootctl/meson.build index 06137bdae00ce..958a804b80598 100644 --- a/src/bootctl/meson.build +++ b/src/bootctl/meson.build @@ -24,7 +24,10 @@ executables += [ ], 'sources' : bootctl_sources, 'link_with' : boot_link_with, - 'dependencies' : [libopenssl_cflags], + 'dependencies' : [ + libopenssl_cflags, + tpm2_cflags, + ], }, test_template + { 'sources' : files( diff --git a/src/core/meson.build b/src/core/meson.build index 26c84fa78b2f2..e46698b4cf8ec 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -174,14 +174,18 @@ libcore_static = static_library( include_directories : core_includes, implicit_include_directories : false, c_args : ['-fvisibility=default'], - dependencies : [libaudit_cflags, - libbpf_cflags, - libcryptsetup_cflags, - libm, - libmount_cflags, - libseccomp_cflags, - libselinux_cflags, - userspace], + dependencies : [ + libacl_cflags, + libaudit_cflags, + libbpf_cflags, + libcryptsetup_cflags, + libm, + libmount_cflags, + libpcre2_cflags, + libseccomp_cflags, + libselinux_cflags, + userspace, + ], build_by_default : false) libcore = shared_library( @@ -210,6 +214,7 @@ core_libs_shared = [ systemd_deps = [ libapparmor_cflags, + libaudit_cflags, libkmod_cflags, libmount_cflags, libseccomp_cflags, @@ -226,6 +231,7 @@ if conf.get('SYSTEMD_MULTICALL_BINARY') == 1 systemd_deps += [ libbpf_cflags, libcryptsetup_cflags, + libopenssl_cflags, libpam_cflags, ] endif @@ -282,6 +288,7 @@ else libbpf_cflags, libcryptsetup_cflags, libmount_cflags, + libopenssl_cflags, libpam_cflags, libseccomp_cflags, libselinux_cflags, diff --git a/src/coredump/meson.build b/src/coredump/meson.build index b0753d86fa882..b1286f80ff679 100644 --- a/src/coredump/meson.build +++ b/src/coredump/meson.build @@ -38,7 +38,10 @@ executables += [ 'sources' : systemd_coredump_sources, 'extract' : systemd_coredump_extract_sources, 'link_with' : [libshared], - 'dependencies' : common_dependencies, + 'dependencies' : [ + common_dependencies, + libacl_cflags, + ], }, executable_template + { 'name' : 'coredumpctl', diff --git a/src/creds/meson.build b/src/creds/meson.build index c18fe2ec8901d..6a14f8e0750bf 100644 --- a/src/creds/meson.build +++ b/src/creds/meson.build @@ -12,6 +12,7 @@ executables += [ 'dependencies' : [ libmount_cflags, libopenssl_cflags, + tpm2_cflags, ], }, ] diff --git a/src/cryptenroll/meson.build b/src/cryptenroll/meson.build index 3fbb5bf080bcc..f0d08ea56c2fd 100644 --- a/src/cryptenroll/meson.build +++ b/src/cryptenroll/meson.build @@ -27,6 +27,7 @@ executables += [ libfido2_cflags, libopenssl_cflags, libp11kit_cflags, + tpm2_cflags, ], }, ] diff --git a/src/cryptsetup/cryptsetup-tokens/meson.build b/src/cryptsetup/cryptsetup-tokens/meson.build index 772c29f50f526..078062d073f1c 100644 --- a/src/cryptsetup/cryptsetup-tokens/meson.build +++ b/src/cryptsetup/cryptsetup-tokens/meson.build @@ -5,7 +5,10 @@ lib_cryptsetup_token_common = static_library( 'cryptsetup-token-util.c', include_directories : includes, implicit_include_directories : false, - dependencies : userspace, + dependencies : [ + libcryptsetup_cflags, + userspace, + ], link_with : libshared, build_by_default : false) diff --git a/src/cryptsetup/meson.build b/src/cryptsetup/meson.build index 9b7f3fa344da5..85f15885c021f 100644 --- a/src/cryptsetup/meson.build +++ b/src/cryptsetup/meson.build @@ -23,11 +23,15 @@ executables += [ libmount_cflags, libopenssl_cflags, libp11kit_cflags, + tpm2_cflags, ], }, generator_template + { 'name' : 'systemd-cryptsetup-generator', 'sources' : files('cryptsetup-generator.c'), + 'dependencies' : [ + libcryptsetup_cflags, + ], }, ] diff --git a/src/dissect/meson.build b/src/dissect/meson.build index 24943cc802905..92e23058979b5 100644 --- a/src/dissect/meson.build +++ b/src/dissect/meson.build @@ -9,6 +9,9 @@ executables += [ 'name' : 'systemd-dissect', 'public' : true, 'sources' : files('dissect.c'), + 'dependencies' : [ + libarchive_cflags, + ], }, ] diff --git a/src/fuzz/meson.build b/src/fuzz/meson.build index 65fc6896f1f77..10848a77e6fc7 100644 --- a/src/fuzz/meson.build +++ b/src/fuzz/meson.build @@ -8,7 +8,6 @@ simple_fuzzers += files( 'fuzz-env-file.c', 'fuzz-hostname-setup.c', 'fuzz-json.c', - 'fuzz-pe-binary.c', 'fuzz-time-util.c', 'fuzz-udev-database.c', 'fuzz-user-record.c', @@ -16,6 +15,13 @@ simple_fuzzers += files( 'fuzz-varlink-idl.c', ) +executables += [ + fuzz_template + { + 'sources' : files('fuzz-pe-binary.c'), + 'dependencies' : libopenssl_cflags, + }, +] + # The following fuzzers do not work on oss-fuzz. See #11018. if not want_ossfuzz simple_fuzzers += files('fuzz-compress.c') diff --git a/src/growfs/meson.build b/src/growfs/meson.build index 0fcedb19d96d3..7bdc329c36dbb 100644 --- a/src/growfs/meson.build +++ b/src/growfs/meson.build @@ -4,6 +4,9 @@ executables += [ libexec_template + { 'name' : 'systemd-growfs', 'sources' : files('growfs.c'), + 'dependencies' : [ + libcryptsetup_cflags, + ], }, libexec_template + { 'name' : 'systemd-makefs', diff --git a/src/home/meson.build b/src/home/meson.build index b724517ae9ce9..2713b0463514b 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -75,7 +75,9 @@ executables += [ 'objects' : ['systemd-homed'], 'dependencies' : [ libblkid_cflags, + libcryptsetup_cflags, libfdisk_cflags, + libfido2_cflags, libopenssl_cflags, libp11kit_cflags, ], @@ -87,6 +89,7 @@ executables += [ 'extract' : homectl_extract, 'objects' : ['systemd-homed'], 'dependencies' : [ + libfido2_cflags, libopenssl_cflags, libp11kit_cflags, ], diff --git a/src/imds/meson.build b/src/imds/meson.build index a23735d100219..9167600b651ef 100644 --- a/src/imds/meson.build +++ b/src/imds/meson.build @@ -10,6 +10,9 @@ executables += [ 'public' : true, 'sources' : files('imdsd.c'), 'extract' : files('imds-util.c'), + 'dependencies' : [ + libcurl_cflags, + ], }, libexec_template + { 'name' : 'systemd-imds', diff --git a/src/import/meson.build b/src/import/meson.build index f133f276b4be2..e141ffb9750f5 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -17,6 +17,10 @@ executables += [ 'import-common.c', 'qcow2-util.c', ), + 'dependencies' : [ + libarchive_cflags, + libselinux_cflags, + ], }, libexec_template + { 'name' : 'systemd-pull', @@ -30,7 +34,10 @@ executables += [ 'pull-tar.c', ), 'objects' : ['systemd-importd'], - 'dependencies' : libopenssl_cflags, + 'dependencies' : [ + libcurl_cflags, + libopenssl_cflags, + ], }, libexec_template + { 'name' : 'systemd-import', @@ -73,6 +80,9 @@ executables += [ test_template + { 'sources' : files('test-tar.c'), 'type' : 'manual', + 'dependencies' : [ + libarchive_cflags, + ], }, test_template + { 'sources' : files('test-qcow2.c'), diff --git a/src/journal-remote/meson.build b/src/journal-remote/meson.build index 22ac8703b55d4..1338a3e673cef 100644 --- a/src/journal-remote/meson.build +++ b/src/journal-remote/meson.build @@ -59,7 +59,10 @@ executables += [ 'sources' : systemd_journal_upload_sources, 'extract' : systemd_journal_upload_extract_sources, 'objects' : ['systemd-journal-remote'], - 'dependencies' : common_deps, + 'dependencies' : [ + common_deps, + libcurl_cflags, + ], }, test_template + { 'sources' : files('test-journal-header-util.c'), diff --git a/src/journal/audit_type-to-name.awk b/src/journal/audit_type-to-name.awk index ef834ff15eaf1..879156fd2f097 100644 --- a/src/journal/audit_type-to-name.awk +++ b/src/journal/audit_type-to-name.awk @@ -2,6 +2,9 @@ BEGIN{ print "#if HAVE_AUDIT" + print "# ifndef SYSTEMD_CFLAGS_MARKER_LIBAUDIT" + print "# error \"missing libaudit_cflags in meson dependency.\"" + print "# endif" print "# include " print "#endif" print "" diff --git a/src/journal/meson.build b/src/journal/meson.build index 3579265a11931..41561deb8b3b7 100644 --- a/src/journal/meson.build +++ b/src/journal/meson.build @@ -95,7 +95,10 @@ executables += [ 'sources' : systemd_journald_sources, 'extract' : systemd_journald_extract_sources, 'dependencies' : [ + libacl_cflags, + libaudit_cflags, liblz4_cflags, + libpcre2_cflags, libselinux_cflags, libxz_cflags, libzstd_cflags, @@ -106,6 +109,9 @@ executables += [ 'public' : true, 'conditions' : ['HAVE_QRENCODE'], 'sources' : files('bsod.c'), + 'dependencies' : [ + libqrencode_cflags, + ], }, executable_template + { 'name' : 'systemd-cat', @@ -121,6 +127,7 @@ executables += [ 'dependencies' : [ liblz4_cflags, libopenssl_cflags, + libpcre2_cflags, libxz_cflags, libzstd_cflags, ], diff --git a/src/libsystemd/sd-bus/test-bus-marshal.c b/src/libsystemd/sd-bus/test-bus-marshal.c index 05074fd92b2f6..064b76d3e000e 100644 --- a/src/libsystemd/sd-bus/test-bus-marshal.c +++ b/src/libsystemd/sd-bus/test-bus-marshal.c @@ -5,12 +5,18 @@ #include "macro.h" #if HAVE_GLIB +#ifndef SYSTEMD_CFLAGS_MARKER_LIBGLIB +# error "missing libglib_cflags in meson dependency." +#endif DISABLE_WARNING_FORMAT_NONLITERAL #include /* NOLINT */ REENABLE_WARNING #endif #if HAVE_DBUS +#ifndef SYSTEMD_CFLAGS_MARKER_LIBDBUS +# error "missing libdbus_cflags in meson dependency." +#endif #include #endif diff --git a/src/locale/xkbcommon-util.h b/src/locale/xkbcommon-util.h index 1a709ae50c03e..e441e9d89bac6 100644 --- a/src/locale/xkbcommon-util.h +++ b/src/locale/xkbcommon-util.h @@ -5,6 +5,10 @@ #include "shared-forward.h" #if HAVE_XKBCOMMON +#ifndef SYSTEMD_CFLAGS_MARKER_LIBXKBCOMMON +# error "missing libxkbcommon_cflags in meson dependency." +#endif + #include extern DLSYM_PROTOTYPE(xkb_context_new); diff --git a/src/login/meson.build b/src/login/meson.build index 44325ccd7c02f..effb44c3a8314 100644 --- a/src/login/meson.build +++ b/src/login/meson.build @@ -48,6 +48,9 @@ executables += [ 'dbus' : true, 'sources' : systemd_logind_sources, 'extract' : systemd_logind_extract_sources, + 'dependencies' : [ + libacl_cflags, + ], }, executable_template + { 'name' : 'loginctl', diff --git a/src/measure/meson.build b/src/measure/meson.build index ff777dc5d9842..3dd793e8c3574 100644 --- a/src/measure/meson.build +++ b/src/measure/meson.build @@ -9,6 +9,9 @@ executables += [ 'HAVE_TPM2', ], 'sources' : files('measure-tool.c'), - 'dependencies' : libopenssl_cflags, + 'dependencies' : [ + libopenssl_cflags, + tpm2_cflags, + ], }, ] diff --git a/src/network/meson.build b/src/network/meson.build index 110af9511c11b..b620e512df776 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -204,9 +204,12 @@ executables += [ libsystemd_network, networkd_link_with, ], + 'dependencies' : [ + libbpf_cflags, + ], 'bpf_programs': [ 'sysctl-monitor', - ] + ], }, libexec_template + { 'name' : 'systemd-networkd-wait-online', @@ -225,6 +228,9 @@ executables += [ libsystemd_network, networkd_link_with, ], + 'dependencies' : [ + libselinux_cflags, + ], }, libexec_template + { 'name' : 'systemd-network-generator', diff --git a/src/nsresourced/meson.build b/src/nsresourced/meson.build index 1654e1766b1eb..2037d06bc8a41 100644 --- a/src/nsresourced/meson.build +++ b/src/nsresourced/meson.build @@ -24,6 +24,9 @@ executables += [ 'name' : 'systemd-nsresourced', 'sources' : systemd_nsresourced_sources, 'extract' : systemd_nsresourced_extract_sources, + 'dependencies' : [ + libbpf_cflags, + ], 'bpf_programs': ['userns-restrict'], }, libexec_template + { diff --git a/src/repart/meson.build b/src/repart/meson.build index 6d3278c3ca65f..721fe73b9ae9e 100644 --- a/src/repart/meson.build +++ b/src/repart/meson.build @@ -15,9 +15,11 @@ executables += [ ), 'dependencies' : [ libblkid_cflags, + libcryptsetup_cflags, libfdisk_cflags, libmount_cflags, libopenssl_cflags, + tpm2_cflags, ], }, executable_template + { @@ -32,9 +34,11 @@ executables += [ ], 'dependencies' : [ libblkid_cflags, + libcryptsetup_cflags, libfdisk_cflags, libmount_cflags, libopenssl_cflags, + tpm2_cflags, ], }, ] diff --git a/src/report/meson.build b/src/report/meson.build index d0a316c6897d2..8a4716ec21c0a 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -10,6 +10,9 @@ executables += [ 'report-sign.c', 'report-upload.c', ), + 'dependencies' : [ + libcurl_cflags, + ], }, libexec_template + { diff --git a/src/sbsign/authenticode.h b/src/sbsign/authenticode.h index c076fe36241d8..236435a7e96f6 100644 --- a/src/sbsign/authenticode.h +++ b/src/sbsign/authenticode.h @@ -1,6 +1,10 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#ifndef SYSTEMD_CFLAGS_MARKER_LIBOPENSSL +# error "missing libopenssl_cflags in meson dependency." +#endif + #include #include "shared-forward.h" diff --git a/src/shared/acl-util.h b/src/shared/acl-util.h index 981f43a8cc082..2b9ac81736653 100644 --- a/src/shared/acl-util.h +++ b/src/shared/acl-util.h @@ -6,6 +6,10 @@ #include "shared-forward.h" #if HAVE_ACL +#ifndef SYSTEMD_CFLAGS_MARKER_LIBACL +# error "missing libacl_cflags in meson dependency." +#endif + #include /* IWYU pragma: export */ #include /* IWYU pragma: export */ diff --git a/src/shared/apparmor-util.h b/src/shared/apparmor-util.h index e87ba84504d7d..2d47a341cb9be 100644 --- a/src/shared/apparmor-util.h +++ b/src/shared/apparmor-util.h @@ -4,6 +4,10 @@ #include "shared-forward.h" #if HAVE_APPARMOR +# ifndef SYSTEMD_CFLAGS_MARKER_LIBAPPARMOR +# error "missing libapparmor_cflags in meson dependency." +# endif + # include # include "dlfcn-util.h" diff --git a/src/shared/blkid-util.h b/src/shared/blkid-util.h index c211bbf3abc0f..97356477fadfa 100644 --- a/src/shared/blkid-util.h +++ b/src/shared/blkid-util.h @@ -6,6 +6,9 @@ #include "shared-forward.h" #if HAVE_BLKID +# ifndef SYSTEMD_CFLAGS_MARKER_LIBBLKID +# error "missing libblkid_cflags in meson dependency." +# endif #include diff --git a/src/shared/bpf-link.h b/src/shared/bpf-link.h index 4de95eb2e1ee3..b5be07865d391 100644 --- a/src/shared/bpf-link.h +++ b/src/shared/bpf-link.h @@ -2,6 +2,9 @@ #pragma once #if HAVE_LIBBPF +#ifndef SYSTEMD_CFLAGS_MARKER_LIBBPF +# error "missing libbpf_cflags in meson dependency." +#endif #include diff --git a/src/shared/bpf-util.h b/src/shared/bpf-util.h index faf7349b8a079..6c2e7a2c98e43 100644 --- a/src/shared/bpf-util.h +++ b/src/shared/bpf-util.h @@ -6,6 +6,9 @@ #include "sd-dlopen.h" #if HAVE_LIBBPF +#ifndef SYSTEMD_CFLAGS_MARKER_LIBBPF +# error "missing libbpf_cflags in meson dependency." +#endif #include /* IWYU pragma: export */ #include /* IWYU pragma: export */ diff --git a/src/shared/crypto-util.h b/src/shared/crypto-util.h index 42a3f2f8c0e61..9927dfb5c7bc3 100644 --- a/src/shared/crypto-util.h +++ b/src/shared/crypto-util.h @@ -33,6 +33,10 @@ int dlopen_libcrypto(int log_level); #define X509_FINGERPRINT_SIZE SHA256_DIGEST_SIZE #if HAVE_OPENSSL +#ifndef SYSTEMD_CFLAGS_MARKER_LIBOPENSSL +# error "missing libopenssl_cflags in meson dependency." +#endif + #define LIBCRYPTO_NOTE(priority) \ SD_ELF_NOTE_DLOPEN("libcrypto", \ "Support for cryptographic operations", \ diff --git a/src/shared/cryptsetup-util.h b/src/shared/cryptsetup-util.h index 1b3ab8b9604f0..6e9c95aa9e8f6 100644 --- a/src/shared/cryptsetup-util.h +++ b/src/shared/cryptsetup-util.h @@ -7,6 +7,10 @@ #include "shared-forward.h" #if HAVE_LIBCRYPTSETUP +#ifndef SYSTEMD_CFLAGS_MARKER_LIBCRYPTSETUP +# error "missing libcryptsetup_cflags in meson dependency." +#endif + #include /* IWYU pragma: export */ /* Available since libcryptsetup 2.7. Always redeclare so DLSYM_PROTOTYPE's typeof() resolves on older diff --git a/src/shared/curl-util.h b/src/shared/curl-util.h index 33bf3684c9eea..a010cefac1046 100644 --- a/src/shared/curl-util.h +++ b/src/shared/curl-util.h @@ -6,6 +6,10 @@ #include "shared-forward.h" #if HAVE_LIBCURL +#ifndef SYSTEMD_CFLAGS_MARKER_LIBCURL +# error "missing libcurl_cflags in meson dependency." +#endif + #include /* IWYU pragma: export */ #include "dlfcn-util.h" diff --git a/src/shared/elf-util.c b/src/shared/elf-util.c index 2210ab39f1423..5e40541ffcf9b 100644 --- a/src/shared/elf-util.c +++ b/src/shared/elf-util.c @@ -1,6 +1,10 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #if HAVE_ELFUTILS +#ifndef SYSTEMD_CFLAGS_MARKER_LIBELF +# error "missing libelf_cflags in meson dependency." +#endif + #include #include #include diff --git a/src/shared/fdisk-util.h b/src/shared/fdisk-util.h index 367c6cd051e44..acdbd7c741cde 100644 --- a/src/shared/fdisk-util.h +++ b/src/shared/fdisk-util.h @@ -6,6 +6,9 @@ #include "shared-forward.h" #if HAVE_LIBFDISK +#ifndef SYSTEMD_CFLAGS_MARKER_LIBFDISK +# error "missing libfdisk_cflags in meson dependency." +#endif #include /* IWYU pragma: export */ diff --git a/src/shared/gnutls-util.h b/src/shared/gnutls-util.h index a110b437c3823..dd0f1d55acb3e 100644 --- a/src/shared/gnutls-util.h +++ b/src/shared/gnutls-util.h @@ -6,6 +6,10 @@ int dlopen_gnutls(int log_level); #if HAVE_GNUTLS +# ifndef SYSTEMD_CFLAGS_MARKER_LIBGNUTLS +# error "missing libgnutls_cflags in meson dependency." +# endif + # include /* IWYU pragma: export */ # include /* IWYU pragma: export */ diff --git a/src/shared/idn-util.h b/src/shared/idn-util.h index 9bed27140fde9..0f3db74c63289 100644 --- a/src/shared/idn-util.h +++ b/src/shared/idn-util.h @@ -6,6 +6,10 @@ #include "shared-forward.h" #if HAVE_LIBIDN2 +#ifndef SYSTEMD_CFLAGS_MARKER_LIBIDN2 +# error "missing libidn2_cflags in meson dependency." +#endif + #include #include "dlfcn-util.h" diff --git a/src/shared/libarchive-util.h b/src/shared/libarchive-util.h index afaf3712007a8..71b4d66d42ce2 100644 --- a/src/shared/libarchive-util.h +++ b/src/shared/libarchive-util.h @@ -6,6 +6,10 @@ #include "shared-forward.h" #if HAVE_LIBARCHIVE +#ifndef SYSTEMD_CFLAGS_MARKER_LIBARCHIVE +# error "missing libarchive_cflags in meson dependency." +#endif + #include /* IWYU pragma: export */ #include /* IWYU pragma: export */ diff --git a/src/shared/libaudit-util.h b/src/shared/libaudit-util.h index 75cef6f1a04f9..d16e25df671ba 100644 --- a/src/shared/libaudit-util.h +++ b/src/shared/libaudit-util.h @@ -6,6 +6,10 @@ int dlopen_libaudit(int log_level); #if HAVE_AUDIT +# ifndef SYSTEMD_CFLAGS_MARKER_LIBAUDIT +# error "missing libaudit_cflags in meson dependency." +# endif + # include /* IWYU pragma: export */ # include "dlfcn-util.h" diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index d9710566e6d2e..106d7688fe958 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -1,6 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #if HAVE_LIBCRYPT +# ifndef SYSTEMD_CFLAGS_MARKER_LIBCRYPT +# error "missing libcrypt_cflags in meson dependency." +# endif # include #endif diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index 12a0201332fa4..82705a7f23dd7 100644 --- a/src/shared/libfido2-util.h +++ b/src/shared/libfido2-util.h @@ -19,6 +19,10 @@ typedef enum Fido2EnrollFlags { int dlopen_libfido2(int log_level); #if HAVE_LIBFIDO2 +#ifndef SYSTEMD_CFLAGS_MARKER_LIBFIDO2 +# error "missing libfido2_cflags in meson dependency." +#endif + #include #include "dlfcn-util.h" diff --git a/src/shared/libmount-util.h b/src/shared/libmount-util.h index 44e58ec932040..3afdb9737885a 100644 --- a/src/shared/libmount-util.h +++ b/src/shared/libmount-util.h @@ -6,6 +6,9 @@ #include "shared-forward.h" #if HAVE_LIBMOUNT +#ifndef SYSTEMD_CFLAGS_MARKER_LIBMOUNT +# error "missing libmount_cflags in meson dependency." +#endif /* This needs to be after sys/mount.h */ #include /* IWYU pragma: export */ diff --git a/src/shared/microhttpd-util.h b/src/shared/microhttpd-util.h index 39b2b9267b2e3..c430f9e3d1a26 100644 --- a/src/shared/microhttpd-util.h +++ b/src/shared/microhttpd-util.h @@ -8,6 +8,10 @@ int dlopen_microhttpd(int log_level); #if HAVE_MICROHTTPD +#ifndef SYSTEMD_CFLAGS_MARKER_LIBMICROHTTPD +# error "missing libmicrohttpd_cflags in meson dependency." +#endif + #define MICROHTTPD_NOTE(priority) \ SD_ELF_NOTE_DLOPEN("microhttpd", \ "Support for embedded HTTP server via libmicrohttpd", \ diff --git a/src/shared/module-util.h b/src/shared/module-util.h index 730e14c31b32a..59d2f3b1f1e07 100644 --- a/src/shared/module-util.h +++ b/src/shared/module-util.h @@ -6,6 +6,9 @@ #include "shared-forward.h" #if HAVE_KMOD +#ifndef SYSTEMD_CFLAGS_MARKER_LIBKMOD +# error "missing libkmod_cflags in meson dependency." +#endif #include /* IWYU pragma: export */ diff --git a/src/shared/pam-util.h b/src/shared/pam-util.h index 408c371e8d743..41591a4d36cfc 100644 --- a/src/shared/pam-util.h +++ b/src/shared/pam-util.h @@ -6,6 +6,10 @@ #include "shared-forward.h" #if HAVE_PAM +#ifndef SYSTEMD_CFLAGS_MARKER_LIBPAM +# error "missing libpam_cflags in meson dependency." +#endif + #include #include #include /* IWYU pragma: export */ diff --git a/src/shared/password-quality-util-passwdqc.c b/src/shared/password-quality-util-passwdqc.c index f32245d344cbe..8d762f7e5cc6d 100644 --- a/src/shared/password-quality-util-passwdqc.c +++ b/src/shared/password-quality-util-passwdqc.c @@ -6,6 +6,9 @@ #include "log.h" /* IWYU pragma: keep */ #if HAVE_PASSWDQC +#ifndef SYSTEMD_CFLAGS_MARKER_LIBPWQUALITY +# error "missing libpwquality_cflags in meson dependency." +#endif #include diff --git a/src/shared/password-quality-util-pwquality.c b/src/shared/password-quality-util-pwquality.c index 34d9b1aa16e26..9952c4d195b27 100644 --- a/src/shared/password-quality-util-pwquality.c +++ b/src/shared/password-quality-util-pwquality.c @@ -6,6 +6,9 @@ #include "log.h" #if HAVE_PWQUALITY +#ifndef SYSTEMD_CFLAGS_MARKER_LIBPWQUALITY +# error "missing libpwquality_cflags in meson dependency." +#endif #include #include diff --git a/src/shared/pcre2-util.h b/src/shared/pcre2-util.h index ba8827fb0749b..6b24dc49824e0 100644 --- a/src/shared/pcre2-util.h +++ b/src/shared/pcre2-util.h @@ -4,6 +4,9 @@ #include "shared-forward.h" #if HAVE_PCRE2 +#ifndef SYSTEMD_CFLAGS_MARKER_LIBPCRE2 +# error "missing libpcre2_cflags in meson dependency." +#endif #include "dlfcn-util.h" diff --git a/src/shared/pkcs11-util.h b/src/shared/pkcs11-util.h index f4599a50f16c6..141fd29c338be 100644 --- a/src/shared/pkcs11-util.h +++ b/src/shared/pkcs11-util.h @@ -2,6 +2,9 @@ #pragma once #if HAVE_P11KIT +# ifndef SYSTEMD_CFLAGS_MARKER_LIBP11KIT +# error "missing libp11kit_cflags in meson dependency." +# endif # include /* IWYU pragma: export */ # include /* IWYU pragma: export */ #endif diff --git a/src/shared/qrcode-util.c b/src/shared/qrcode-util.c index 44d674ba6a3ae..a5bf75f88918c 100644 --- a/src/shared/qrcode-util.c +++ b/src/shared/qrcode-util.c @@ -3,6 +3,9 @@ #include "qrcode-util.h" #if HAVE_QRENCODE +#ifndef SYSTEMD_CFLAGS_MARKER_LIBQRENCODE +# error "missing libqrencode_cflags in meson dependency." +#endif #include #endif #include diff --git a/src/shared/reboot-util.c b/src/shared/reboot-util.c index bd04fee342154..758b7a2600b06 100644 --- a/src/shared/reboot-util.c +++ b/src/shared/reboot-util.c @@ -5,6 +5,10 @@ #include #if HAVE_XENCTRL +#ifndef SYSTEMD_CFLAGS_MARKER_LIBXENCTRL +# error "missing libxenctrl_cflags in meson dependency." +#endif + #include #include diff --git a/src/shared/seccomp-util.h b/src/shared/seccomp-util.h index 89dcaf5ca74b0..2bcb2d935bad0 100644 --- a/src/shared/seccomp-util.h +++ b/src/shared/seccomp-util.h @@ -7,6 +7,10 @@ #include "shared-forward.h" #if HAVE_SECCOMP +#ifndef SYSTEMD_CFLAGS_MARKER_LIBSECCOMP +# error "missing libseccomp_cflags in meson dependency." +#endif + #include /* IWYU pragma: export */ #include "dlfcn-util.h" diff --git a/src/shared/selinux-util.h b/src/shared/selinux-util.h index 98cfb6bbe40b4..3cc48dd3acd92 100644 --- a/src/shared/selinux-util.h +++ b/src/shared/selinux-util.h @@ -8,6 +8,10 @@ #include "shared-forward.h" #if HAVE_SELINUX +#ifndef SYSTEMD_CFLAGS_MARKER_LIBSELINUX +# error "missing libselinux_cflags in meson dependency." +#endif + #include #include #include diff --git a/src/shared/ssl-util.h b/src/shared/ssl-util.h index 857d4b5e17356..3a9a4084da34d 100644 --- a/src/shared/ssl-util.h +++ b/src/shared/ssl-util.h @@ -8,6 +8,10 @@ int dlopen_libssl(int log_level); #if HAVE_OPENSSL +#ifndef SYSTEMD_CFLAGS_MARKER_LIBOPENSSL +# error "missing libopenssl_cflags in meson dependency." +#endif + #define LIBSSL_NOTE(priority) \ SD_ELF_NOTE_DLOPEN("libssl", \ "Support for TLS", \ diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index eb910b1e84c3b..88098acac8ca7 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -50,6 +50,10 @@ static inline bool TPM2_PCR_MASK_VALID(uint32_t pcr_mask) { int dlopen_tpm2(int log_level); #if HAVE_TPM2 +#ifndef SYSTEMD_CFLAGS_MARKER_TPM2 +# error "missing tpm2_cflags in meson dependency." +#endif + #define TPM2_ESYS_NOTE(priority) \ SD_ELF_NOTE_DLOPEN("tpm", "Support for TPM", priority, "libtss2-esys.so.0") #define TPM2_RC_NOTE(priority) \ diff --git a/src/sysext/meson.build b/src/sysext/meson.build index 75d06163c0c5e..2df28f4398581 100644 --- a/src/sysext/meson.build +++ b/src/sysext/meson.build @@ -10,6 +10,7 @@ executables += [ libblkid_cflags, libcryptsetup_cflags, libmount_cflags, + libselinux_cflags, ], }, ] diff --git a/src/systemctl/meson.build b/src/systemctl/meson.build index 882704c9d7d87..c287f8a490384 100644 --- a/src/systemctl/meson.build +++ b/src/systemctl/meson.build @@ -65,6 +65,9 @@ executables += [ 'sources' : files('fuzz-systemctl-parse-argv.c'), 'objects' : ['systemctl'], 'link_with' : systemctl_link_with, + 'dependencies' : [ + libselinux_cflags, + ], }, ] diff --git a/src/test/gcrypt-util.h b/src/test/gcrypt-util.h index 03404886974ce..37eb3f5b61922 100644 --- a/src/test/gcrypt-util.h +++ b/src/test/gcrypt-util.h @@ -11,6 +11,10 @@ int dlopen_gcrypt(int log_level); int initialize_libgcrypt(bool secmem); #if HAVE_GCRYPT +#ifndef SYSTEMD_CFLAGS_MARKER_LIBGCRYPT +# error "missing libgcrypt_cflags in meson dependency." +#endif + #define GCRYPT_NOTE(priority) \ SD_ELF_NOTE_DLOPEN("gcrypt", \ "Support for journald forward-sealing", \ diff --git a/src/test/meson.build b/src/test/meson.build index c90235e078949..d7353ce8e2ce5 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -79,13 +79,11 @@ simple_tests += files( 'test-clock.c', 'test-color-util.c', 'test-compare-operator.c', - 'test-condition.c', 'test-conf-files.c', 'test-conf-parser.c', 'test-copy.c', 'test-coredump-util.c', 'test-cpu-set-util.c', - 'test-creds.c', 'test-daemon.c', 'test-data-fd-util.c', 'test-date.c', @@ -170,7 +168,6 @@ simple_tests += files( 'test-parse-util.c', 'test-path-lookup.c', 'test-path-util.c', - 'test-pe-binary.c', 'test-percent-util.c', 'test-pidref.c', 'test-pressure.c', @@ -246,6 +243,7 @@ executables += [ test_template + { 'sources' : files('test-acl-util.c'), 'conditions' : ['HAVE_ACL'], + 'dependencies' : libacl_cflags, }, test_template + { 'sources' : files('test-af-list.c') + @@ -280,6 +278,14 @@ executables += [ 'sources' : files('test-compress.c'), 'link_with' : [libshared], }, + test_template + { + 'sources' : files('test-condition.c'), + 'dependencies' : [ + libapparmor_cflags, + libaudit_cflags, + libselinux_cflags, + ], + }, test_template + { 'sources' : files('test-coredump-stacktrace.c'), 'type' : 'manual', @@ -291,6 +297,10 @@ executables += [ 'override_options' : ['b_sanitize=none', 'strip=false', 'debug=true'], 'c_args' : ['-fno-sanitize=all', '-fno-optimize-sibling-calls', '-O1'], }, + test_template + { + 'sources' : files('test-creds.c'), + 'dependencies' : tpm2_cflags, + }, test_template + { 'sources' : files('test-crypto-util.c'), 'dependencies' : libopenssl_cflags, @@ -306,15 +316,29 @@ executables += [ 'gcrypt-util.c', ), 'dependencies' : [ + libacl_cflags, + libapparmor_cflags, + libarchive_cflags, + libaudit_cflags, libblkid_cflags, + libbpf_cflags, + libcryptsetup_cflags, + libcurl_cflags, libfdisk_cflags, + libfido2_cflags, libgcrypt_cflags, libgnutls_cflags, + libidn2_cflags, libkmod_cflags, libmicrohttpd_cflags, libmount_cflags, + libopenssl_cflags, libp11kit_cflags, + libpam_cflags, + libpcre2_cflags, libseccomp_cflags, + libselinux_cflags, + tpm2_cflags, ], }, test_template + { @@ -373,6 +397,7 @@ executables += [ test_template + { 'sources' : files('test-curl-util.c'), 'conditions' : ['HAVE_LIBCURL'], + 'dependencies' : libcurl_cflags, }, test_template + { 'sources' : files('test-libcrypt-util.c'), @@ -423,6 +448,10 @@ executables += [ 'objects' : ['test-nss-hosts'], 'conditions' : ['ENABLE_NSS'], }, + test_template + { + 'sources' : files('test-pe-binary.c'), + 'dependencies' : libopenssl_cflags, + }, test_template + { 'sources' : files('test-pkcs7-util.c'), 'dependencies' : libopenssl_cflags, @@ -476,7 +505,10 @@ executables += [ }, test_template + { 'sources' : files('test-tpm2.c'), - 'dependencies' : libopenssl_cflags, + 'dependencies' : [ + libopenssl_cflags, + tpm2_cflags, + ], 'timeout' : 120, }, test_template + { @@ -528,11 +560,17 @@ executables += [ }, core_test_template + { 'sources' : files('test-bpf-restrict-fs.c'), - 'dependencies' : common_test_dependencies, + 'dependencies' : [ + common_test_dependencies, + libbpf_cflags, + ], }, core_test_template + { 'sources' : files('test-bpf-restrict-fsaccess.c'), - 'dependencies' : common_test_dependencies, + 'dependencies' : [ + common_test_dependencies, + libbpf_cflags, + ], 'bpf_programs' : ['restrict-fsaccess'], 'type' : 'manual', }, @@ -584,7 +622,10 @@ executables += [ }, core_test_template + { 'sources' : files('test-load-fragment.c'), - 'dependencies' : common_test_dependencies, + 'dependencies' : [ + common_test_dependencies, + libpcre2_cflags, + ], }, core_test_template + { 'sources' : files('test-loop-util.c'), diff --git a/src/tmpfiles/meson.build b/src/tmpfiles/meson.build index 2a7728c295533..fcf2749e4bf2b 100644 --- a/src/tmpfiles/meson.build +++ b/src/tmpfiles/meson.build @@ -12,7 +12,10 @@ executables += [ 'public' : true, 'extract' : files('tmpfiles.c') + offline_passwd_c, - 'dependencies' : libacl_cflags, + 'dependencies' : [ + libacl_cflags, + libselinux_cflags, + ], }, executable_template + { 'name' : 'systemd-tmpfiles.standalone', @@ -24,7 +27,10 @@ executables += [ libshared_static, libsystemd_static, ], - 'dependencies' : libacl_cflags, + 'dependencies' : [ + libacl_cflags, + libselinux_cflags, + ], }, test_template + { 'sources' : files('test-offline-passwd.c') + diff --git a/src/tpm2-setup/meson.build b/src/tpm2-setup/meson.build index 55f0ce26d31cb..bb3c3338ba251 100644 --- a/src/tpm2-setup/meson.build +++ b/src/tpm2-setup/meson.build @@ -11,6 +11,7 @@ executables += [ ], 'dependencies' : [ libopenssl_cflags, + tpm2_cflags, ], }, libexec_template + { @@ -21,6 +22,9 @@ executables += [ 'HAVE_OPENSSL', 'HAVE_TPM2', ], + 'dependencies' : [ + tpm2_cflags, + ], }, libexec_template + { 'name' : 'systemd-tpm2-swtpm', @@ -39,6 +43,9 @@ executables += [ 'HAVE_OPENSSL', 'HAVE_TPM2', ], + 'dependencies' : [ + tpm2_cflags, + ], }, ] diff --git a/src/udev/meson.build b/src/udev/meson.build index 375645e7593ed..6c2232c74c6dc 100644 --- a/src/udev/meson.build +++ b/src/udev/meson.build @@ -122,6 +122,8 @@ udev_dependencies = [ libblkid_cflags, libkmod_cflags, libmount_cflags, + libselinux_cflags, + tpm2_cflags, ] udev_plugin_template = executable_template + { diff --git a/src/veritysetup/meson.build b/src/veritysetup/meson.build index 21432d41f1658..437e58ecd834e 100644 --- a/src/veritysetup/meson.build +++ b/src/veritysetup/meson.build @@ -8,7 +8,10 @@ executables += [ libexec_template + { 'name' : 'systemd-veritysetup', 'sources' : files('veritysetup.c'), - 'dependencies' : libcryptsetup_cflags, + 'dependencies' : [ + libcryptsetup_cflags, + tpm2_cflags, + ], }, generator_template + { 'name' : 'systemd-veritysetup-generator', From 738884d9f7584dbd559c22d3dfbca4a5a6af3405 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 3 Jul 2026 17:07:29 +0900 Subject: [PATCH 46/71] ci/build-test: add build test for -Dsystemd-multicall-binary=true --- .github/workflows/build-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.sh b/.github/workflows/build-test.sh index 084e2c4f1af16..6d8324b9458f2 100755 --- a/.github/workflows/build-test.sh +++ b/.github/workflows/build-test.sh @@ -15,7 +15,7 @@ ARGS=( "--optimization=2 -Ddns-over-tls=openssl -Dc_args='-DOPENSSL_NO_DEPRECATED'" "--optimization=3 -Db_lto=true -Ddns-over-tls=false" "--optimization=3 -Db_lto=false -Dtpm2=disabled -Dlibfido2=disabled -Dp11kit=disabled -Defi=false -Dbootloader=disabled" - "--optimization=3 -Dfexecve=true -Dstandalone-binaries=true -Dstatic-libsystemd=true -Dstatic-libudev=true" + "--optimization=3 -Dfexecve=true -Dstandalone-binaries=true -Dstatic-libsystemd=true -Dstatic-libudev=true -Dsystemd-multicall-binary=true" "-Db_ndebug=true" "--auto-features=disabled -Dcompat-mutable-uid-boundaries=true" ) From 713a2cf79c598c32545f6bcb6cf7e22a3c433050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 2 Jul 2026 16:32:44 +0200 Subject: [PATCH 47/71] test-socket-util: convert to new ASSERT macros Replace assert_se() with the typed ASSERT_* macros throughout, matching the conversions done across the rest of src/test/. --- src/test/test-socket-util.c | 290 ++++++++++++++++-------------------- 1 file changed, 131 insertions(+), 159 deletions(-) diff --git a/src/test/test-socket-util.c b/src/test/test-socket-util.c index 4b14bd46960d1..38f6573ea1209 100644 --- a/src/test/test-socket-util.c +++ b/src/test/test-socket-util.c @@ -27,45 +27,45 @@ assert_cc(SUN_PATH_LEN == 108); TEST(ifname_valid) { - assert_se( ifname_valid("foo")); - assert_se( ifname_valid("eth0")); - - assert_se(!ifname_valid("0")); - assert_se(!ifname_valid("99")); - assert_se( ifname_valid("a99")); - assert_se( ifname_valid("99a")); - - assert_se(!ifname_valid(NULL)); - assert_se(!ifname_valid("")); - assert_se(!ifname_valid(" ")); - assert_se(!ifname_valid(" foo")); - assert_se(!ifname_valid("bar\n")); - assert_se(!ifname_valid(".")); - assert_se(!ifname_valid("..")); - assert_se(ifname_valid("foo.bar")); - assert_se(!ifname_valid("x:y")); - - assert_se( ifname_valid_full("xxxxxxxxxxxxxxx", 0)); - assert_se(!ifname_valid_full("xxxxxxxxxxxxxxxx", 0)); - assert_se( ifname_valid_full("xxxxxxxxxxxxxxxx", IFNAME_VALID_ALTERNATIVE)); - assert_se( ifname_valid_full("xxxxxxxxxxxxxxxx", IFNAME_VALID_ALTERNATIVE)); - assert_se(!ifname_valid_full("999", IFNAME_VALID_ALTERNATIVE)); - assert_se( ifname_valid_full("999", IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC)); - assert_se(!ifname_valid_full("0", IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC)); + ASSERT_TRUE(ifname_valid("foo")); + ASSERT_TRUE(ifname_valid("eth0")); + + ASSERT_FALSE(ifname_valid("0")); + ASSERT_FALSE(ifname_valid("99")); + ASSERT_TRUE(ifname_valid("a99")); + ASSERT_TRUE(ifname_valid("99a")); + + ASSERT_FALSE(ifname_valid(NULL)); + ASSERT_FALSE(ifname_valid("")); + ASSERT_FALSE(ifname_valid(" ")); + ASSERT_FALSE(ifname_valid(" foo")); + ASSERT_FALSE(ifname_valid("bar\n")); + ASSERT_FALSE(ifname_valid(".")); + ASSERT_FALSE(ifname_valid("..")); + ASSERT_TRUE(ifname_valid("foo.bar")); + ASSERT_FALSE(ifname_valid("x:y")); + + ASSERT_TRUE(ifname_valid_full("xxxxxxxxxxxxxxx", 0)); + ASSERT_FALSE(ifname_valid_full("xxxxxxxxxxxxxxxx", 0)); + ASSERT_TRUE(ifname_valid_full("xxxxxxxxxxxxxxxx", IFNAME_VALID_ALTERNATIVE)); + ASSERT_TRUE(ifname_valid_full("xxxxxxxxxxxxxxxx", IFNAME_VALID_ALTERNATIVE)); + ASSERT_FALSE(ifname_valid_full("999", IFNAME_VALID_ALTERNATIVE)); + ASSERT_TRUE(ifname_valid_full("999", IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC)); + ASSERT_FALSE(ifname_valid_full("0", IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC)); } static void test_socket_print_unix_one(const char *in, size_t len_in, const char *expected) { _cleanup_free_ char *out = NULL, *c = NULL; - assert_se(len_in <= SUN_PATH_LEN); + ASSERT_LE(len_in, SUN_PATH_LEN); SocketAddress a = { .sockaddr = { .un = { .sun_family = AF_UNIX } }, .size = offsetof(struct sockaddr_un, sun_path) + len_in, .type = SOCK_STREAM, }; memcpy(a.sockaddr.un.sun_path, in, len_in); - assert_se(socket_address_print(&a, &out) >= 0); - assert_se(c = cescape(in)); + ASSERT_OK(socket_address_print(&a, &out)); + ASSERT_NOT_NULL(c = cescape(in)); log_info("\"%s\" → \"%s\" (expect \"%s\")", in, out, expected); ASSERT_STREQ(out, expected); } @@ -112,13 +112,13 @@ TEST(sockaddr_equal) { .vm.svm_cid = VMADDR_CID_ANY, }; - assert_se(sockaddr_equal(&a, &a)); - assert_se(sockaddr_equal(&a, &b)); - assert_se(sockaddr_equal(&d, &d)); - assert_se(sockaddr_equal(&e, &e)); - assert_se(!sockaddr_equal(&a, &c)); - assert_se(!sockaddr_equal(&b, &c)); - assert_se(!sockaddr_equal(&a, &e)); + ASSERT_TRUE(sockaddr_equal(&a, &a)); + ASSERT_TRUE(sockaddr_equal(&a, &b)); + ASSERT_TRUE(sockaddr_equal(&d, &d)); + ASSERT_TRUE(sockaddr_equal(&e, &e)); + ASSERT_FALSE(sockaddr_equal(&a, &c)); + ASSERT_FALSE(sockaddr_equal(&b, &c)); + ASSERT_FALSE(sockaddr_equal(&a, &e)); } TEST(sockaddr_un_len) { @@ -132,25 +132,25 @@ TEST(sockaddr_un_len) { .sun_path = "\0foobar", }; - assert_se(sockaddr_un_len(&fs) == offsetof(struct sockaddr_un, sun_path) + strlen(fs.sun_path) + 1); - assert_se(sockaddr_un_len(&abstract) == offsetof(struct sockaddr_un, sun_path) + 1 + strlen(abstract.sun_path + 1)); + ASSERT_EQ(sockaddr_un_len(&fs), offsetof(struct sockaddr_un, sun_path) + strlen(fs.sun_path) + 1); + ASSERT_EQ(sockaddr_un_len(&abstract), offsetof(struct sockaddr_un, sun_path) + 1 + strlen(abstract.sun_path + 1)); } TEST(in_addr_is_multicast) { union in_addr_union a, b; int f; - assert_se(in_addr_from_string_auto("192.168.3.11", &f, &a) >= 0); - assert_se(in_addr_is_multicast(f, &a) == 0); + ASSERT_OK(in_addr_from_string_auto("192.168.3.11", &f, &a)); + ASSERT_OK_EQ(in_addr_is_multicast(f, &a), 0); - assert_se(in_addr_from_string_auto("224.0.0.1", &f, &a) >= 0); - assert_se(in_addr_is_multicast(f, &a) == 1); + ASSERT_OK(in_addr_from_string_auto("224.0.0.1", &f, &a)); + ASSERT_OK_EQ(in_addr_is_multicast(f, &a), 1); - assert_se(in_addr_from_string_auto("FF01:0:0:0:0:0:0:1", &f, &b) >= 0); - assert_se(in_addr_is_multicast(f, &b) == 1); + ASSERT_OK(in_addr_from_string_auto("FF01:0:0:0:0:0:0:1", &f, &b)); + ASSERT_OK_EQ(in_addr_is_multicast(f, &b), 1); - assert_se(in_addr_from_string_auto("2001:db8::c:69b:aeff:fe53:743e", &f, &b) >= 0); - assert_se(in_addr_is_multicast(f, &b) == 0); + ASSERT_OK(in_addr_from_string_auto("2001:db8::c:69b:aeff:fe53:743e", &f, &b)); + ASSERT_OK_EQ(in_addr_is_multicast(f, &b), 0); } TEST(getpeercred_getpeergroups) { @@ -173,38 +173,35 @@ TEST(getpeercred_getpeergroups) { test_gids = (gid_t*) gids; n_test_gids = ELEMENTSOF(gids); - assert_se(fully_set_uid_gid(test_uid, test_gid, test_gids, n_test_gids) >= 0); + ASSERT_OK(fully_set_uid_gid(test_uid, test_gid, test_gids, n_test_gids)); } else { test_uid = getuid(); test_gid = getgid(); - int ngroups_max = sysconf_ngroups_max(); - assert_se(ngroups_max > 0); + int ngroups_max = ASSERT_OK_POSITIVE(sysconf_ngroups_max()); test_gids = newa(gid_t, ngroups_max); - r = getgroups(ngroups_max, test_gids); - assert_se(r >= 0); + r = ASSERT_OK_ERRNO(getgroups(ngroups_max, test_gids)); n_test_gids = (size_t) r; } - assert_se(socketpair(AF_UNIX, SOCK_STREAM, 0, pair) >= 0); + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM, 0, pair)); - assert_se(getpeercred(pair[0], &ucred) >= 0); + ASSERT_OK(getpeercred(pair[0], &ucred)); - assert_se(ucred.uid == test_uid); - assert_se(ucred.gid == test_gid); - assert_se(ucred.pid == getpid_cached()); + ASSERT_EQ(ucred.uid, test_uid); + ASSERT_EQ(ucred.gid, test_gid); + ASSERT_EQ(ucred.pid, getpid_cached()); { _cleanup_free_ gid_t *peer_groups = NULL; - r = getpeergroups(pair[0], &peer_groups); - assert_se(r >= 0 || IN_SET(r, -EOPNOTSUPP, -ENOPROTOOPT)); + r = ASSERT_OK_OR(getpeergroups(pair[0], &peer_groups), -EOPNOTSUPP, -ENOPROTOOPT); if (r >= 0) { - assert_se((size_t) r == n_test_gids); - assert_se(memcmp(peer_groups, test_gids, sizeof(gid_t) * n_test_gids) == 0); + ASSERT_EQ((size_t) r, n_test_gids); + ASSERT_EQ(memcmp(peer_groups, test_gids, sizeof(gid_t) * n_test_gids), 0); } } @@ -218,7 +215,7 @@ TEST(passfd_read) { _cleanup_close_pair_ int pair[2] = EBADF_PAIR; int r; - assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0); + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair)); r = ASSERT_OK(pidref_safe_fork("(passfd_read)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL)); @@ -227,13 +224,12 @@ TEST(passfd_read) { pair[0] = safe_close(pair[0]); char tmpfile[] = "/tmp/test-socket-util-passfd-read-XXXXXX"; - assert_se(write_tmpfile(tmpfile, file_contents) == 0); + ASSERT_OK_ZERO(write_tmpfile(tmpfile, file_contents)); - _cleanup_close_ int tmpfd = open(tmpfile, O_RDONLY); - assert_se(tmpfd >= 0); - assert_se(unlink(tmpfile) == 0); + _cleanup_close_ int tmpfd = ASSERT_OK_ERRNO(open(tmpfile, O_RDONLY)); + ASSERT_OK_ERRNO(unlink(tmpfile)); - assert_se(send_one_fd(pair[1], tmpfd, MSG_DONTWAIT) == 0); + ASSERT_OK_ZERO(send_one_fd(pair[1], tmpfd, MSG_DONTWAIT)); _exit(EXIT_SUCCESS); } @@ -244,11 +240,10 @@ TEST(passfd_read) { pair[1] = safe_close(pair[1]); - assert_se(receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd) == 0); + ASSERT_OK_ZERO(receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd)); - assert_se(fd >= 0); - ssize_t n = read(fd, buf, sizeof(buf)-1); - assert_se(n >= 0); + ASSERT_OK(fd); + ssize_t n = ASSERT_OK_ERRNO(read(fd, buf, sizeof(buf)-1)); buf[n] = 0; ASSERT_STREQ(buf, file_contents); } @@ -259,7 +254,7 @@ TEST(passfd_contents_read) { static const char wire_contents[] = "test contents on the wire"; int r; - assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0); + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair)); r = ASSERT_OK(pidref_safe_fork("(passfd_contents_read)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL)); @@ -270,13 +265,12 @@ TEST(passfd_contents_read) { pair[0] = safe_close(pair[0]); - assert_se(write_tmpfile(tmpfile, file_contents) == 0); + ASSERT_OK_ZERO(write_tmpfile(tmpfile, file_contents)); - _cleanup_close_ int tmpfd = open(tmpfile, O_RDONLY); - assert_se(tmpfd >= 0); - assert_se(unlink(tmpfile) == 0); + _cleanup_close_ int tmpfd = ASSERT_OK_ERRNO(open(tmpfile, O_RDONLY)); + ASSERT_OK_ERRNO(unlink(tmpfile)); - assert_se(send_one_fd_iov(pair[1], tmpfd, &iov, 1, MSG_DONTWAIT) > 0); + ASSERT_OK_POSITIVE(send_one_fd_iov(pair[1], tmpfd, &iov, 1, MSG_DONTWAIT)); _exit(EXIT_SUCCESS); } @@ -288,14 +282,12 @@ TEST(passfd_contents_read) { pair[1] = safe_close(pair[1]); - k = receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd); - assert_se(k > 0); + k = ASSERT_OK_POSITIVE(receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd)); buf[k] = 0; ASSERT_STREQ(buf, wire_contents); - assert_se(fd >= 0); - r = read(fd, buf, sizeof(buf)-1); - assert_se(r >= 0); + ASSERT_OK(fd); + r = ASSERT_OK_ERRNO(read(fd, buf, sizeof(buf)-1)); buf[r] = 0; ASSERT_STREQ(buf, file_contents); } @@ -305,10 +297,9 @@ TEST(receive_nopassfd) { static const char wire_contents[] = "no fd passed here"; int r; - assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0); + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair)); r = ASSERT_OK(pidref_safe_fork("(receive_nopassfd)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL)); - assert_se(r >= 0); if (r == 0) { /* Child */ @@ -316,7 +307,7 @@ TEST(receive_nopassfd) { pair[0] = safe_close(pair[0]); - assert_se(send_one_fd_iov(pair[1], -1, &iov, 1, MSG_DONTWAIT) > 0); + ASSERT_OK_POSITIVE(send_one_fd_iov(pair[1], -1, &iov, 1, MSG_DONTWAIT)); _exit(EXIT_SUCCESS); } @@ -328,20 +319,19 @@ TEST(receive_nopassfd) { pair[1] = safe_close(pair[1]); - k = receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd); - assert_se(k > 0); + k = ASSERT_OK_POSITIVE(receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd)); buf[k] = 0; ASSERT_STREQ(buf, wire_contents); /* no fd passed here, confirm it was reset */ - assert_se(fd == -EBADF); + ASSERT_EQ(fd, -EBADF); } TEST(send_nodata_nofd) { _cleanup_close_pair_ int pair[2] = EBADF_PAIR; int r; - assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0); + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair)); r = ASSERT_OK(pidref_safe_fork("(send_nodata_nofd)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL)); @@ -349,7 +339,7 @@ TEST(send_nodata_nofd) { /* Child */ pair[0] = safe_close(pair[0]); - assert_se(send_one_fd_iov(pair[1], -1, NULL, 0, MSG_DONTWAIT) == -EINVAL); + ASSERT_ERROR(send_one_fd_iov(pair[1], -1, NULL, 0, MSG_DONTWAIT), EINVAL); _exit(EXIT_SUCCESS); } @@ -357,23 +347,21 @@ TEST(send_nodata_nofd) { char buf[64]; struct iovec iov = IOVEC_MAKE(buf, sizeof(buf)-1); int fd = -999; - ssize_t k; pair[1] = safe_close(pair[1]); - k = receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd); /* recvmsg() will return errno EAGAIN if nothing was sent */ - assert_se(k == -EAGAIN); + ASSERT_ERROR(receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd), EAGAIN); /* receive_one_fd_iov returned error, so confirm &fd wasn't touched */ - assert_se(fd == -999); + ASSERT_EQ(fd, -999); } TEST(send_emptydata) { _cleanup_close_pair_ int pair[2] = EBADF_PAIR; int r; - assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0); + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair)); r = ASSERT_OK(pidref_safe_fork("(send_emptydata)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL)); @@ -382,7 +370,7 @@ TEST(send_emptydata) { pair[0] = safe_close(pair[0]); /* This will succeed, since iov is set. */ - assert_se(send_one_fd_iov(pair[1], -1, &iovec_empty, 1, MSG_DONTWAIT) == 0); + ASSERT_OK_ZERO(send_one_fd_iov(pair[1], -1, &iovec_empty, 1, MSG_DONTWAIT)); _exit(EXIT_SUCCESS); } @@ -390,16 +378,14 @@ TEST(send_emptydata) { char buf[64]; struct iovec iov = IOVEC_MAKE(buf, sizeof(buf)-1); int fd = -999; - ssize_t k; pair[1] = safe_close(pair[1]); - k = receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd); /* receive_one_fd_iov() returns -EIO if an fd is not found and no data was returned. */ - assert_se(k == -EIO); + ASSERT_ERROR(receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd), EIO); /* receive_one_fd_iov returned error, so confirm &fd wasn't touched */ - assert_se(fd == -999); + ASSERT_EQ(fd, -999); } TEST(flush_accept) { @@ -408,59 +394,49 @@ TEST(flush_accept) { union sockaddr_union lsa; socklen_t l; - listen_stream = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); - assert_se(listen_stream >= 0); - - listen_dgram = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); - assert_se(listen_dgram >= 0); - - listen_seqpacket = socket(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); - assert_se(listen_seqpacket >= 0); - - assert_se(flush_accept(listen_stream) < 0); - assert_se(flush_accept(listen_dgram) < 0); - assert_se(flush_accept(listen_seqpacket) < 0); - - assert_se(bind(listen_stream, &sa.sa, sizeof(sa_family_t)) >= 0); - assert_se(bind(listen_dgram, &sa.sa, sizeof(sa_family_t)) >= 0); - assert_se(bind(listen_seqpacket, &sa.sa, sizeof(sa_family_t)) >= 0); + ASSERT_OK_ERRNO(listen_stream = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)); + ASSERT_OK_ERRNO(listen_dgram = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)); + ASSERT_OK_ERRNO(listen_seqpacket = socket(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)); - assert_se(flush_accept(listen_stream) < 0); - assert_se(flush_accept(listen_dgram) < 0); - assert_se(flush_accept(listen_seqpacket) < 0); + ASSERT_FAIL(flush_accept(listen_stream)); + ASSERT_FAIL(flush_accept(listen_dgram)); + ASSERT_FAIL(flush_accept(listen_seqpacket)); - assert_se(listen(listen_stream, SOMAXCONN_DELUXE) >= 0); - assert_se(listen(listen_dgram, SOMAXCONN_DELUXE) < 0); - assert_se(listen(listen_seqpacket, SOMAXCONN_DELUXE) >= 0); + ASSERT_OK_ERRNO(bind(listen_stream, &sa.sa, sizeof(sa_family_t))); + ASSERT_OK_ERRNO(bind(listen_dgram, &sa.sa, sizeof(sa_family_t))); + ASSERT_OK_ERRNO(bind(listen_seqpacket, &sa.sa, sizeof(sa_family_t))); - assert_se(flush_accept(listen_stream) >= 0); - assert_se(flush_accept(listen_dgram) < 0); - assert_se(flush_accept(listen_seqpacket) >= 0); + ASSERT_FAIL(flush_accept(listen_stream)); + ASSERT_FAIL(flush_accept(listen_dgram)); + ASSERT_FAIL(flush_accept(listen_seqpacket)); - connect_stream = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); - assert_se(connect_stream >= 0); + ASSERT_OK_ERRNO(listen(listen_stream, SOMAXCONN_DELUXE)); + ASSERT_FAIL(listen(listen_dgram, SOMAXCONN_DELUXE)); + ASSERT_OK_ERRNO(listen(listen_seqpacket, SOMAXCONN_DELUXE)); - connect_dgram = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); - assert_se(connect_dgram >= 0); + ASSERT_OK(flush_accept(listen_stream)); + ASSERT_FAIL(flush_accept(listen_dgram)); + ASSERT_OK(flush_accept(listen_seqpacket)); - connect_seqpacket = socket(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); - assert_se(connect_seqpacket >= 0); + ASSERT_OK_ERRNO(connect_stream = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)); + ASSERT_OK_ERRNO(connect_dgram = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)); + ASSERT_OK_ERRNO(connect_seqpacket = socket(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)); l = sizeof(lsa); - assert_se(getsockname(listen_stream, &lsa.sa, &l) >= 0); - assert_se(connect(connect_stream, &lsa.sa, l) >= 0); + ASSERT_OK_ERRNO(getsockname(listen_stream, &lsa.sa, &l)); + ASSERT_OK_ERRNO(connect(connect_stream, &lsa.sa, l)); l = sizeof(lsa); - assert_se(getsockname(listen_dgram, &lsa.sa, &l) >= 0); - assert_se(connect(connect_dgram, &lsa.sa, l) >= 0); + ASSERT_OK_ERRNO(getsockname(listen_dgram, &lsa.sa, &l)); + ASSERT_OK_ERRNO(connect(connect_dgram, &lsa.sa, l)); l = sizeof(lsa); - assert_se(getsockname(listen_seqpacket, &lsa.sa, &l) >= 0); - assert_se(connect(connect_seqpacket, &lsa.sa, l) >= 0); + ASSERT_OK_ERRNO(getsockname(listen_seqpacket, &lsa.sa, &l)); + ASSERT_OK_ERRNO(connect(connect_seqpacket, &lsa.sa, l)); - assert_se(flush_accept(listen_stream) >= 0); - assert_se(flush_accept(listen_dgram) < 0); - assert_se(flush_accept(listen_seqpacket) >= 0); + ASSERT_OK(flush_accept(listen_stream)); + ASSERT_FAIL(flush_accept(listen_dgram)); + ASSERT_OK(flush_accept(listen_seqpacket)); } TEST(ipv6_enabled) { @@ -475,39 +451,35 @@ TEST(sockaddr_un_set_path) { union sockaddr_union sa; _cleanup_close_ int fd1, fd2, fd3; - assert_se(mkdtemp_malloc("/tmp/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaXXXXXX", &t) >= 0); - assert_se(strlen(t) > SUN_PATH_LEN); + ASSERT_OK(mkdtemp_malloc("/tmp/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaXXXXXX", &t)); + ASSERT_GT(strlen(t), SUN_PATH_LEN); - assert_se(j = path_join(t, "sock")); - assert_se(sockaddr_un_set_path(&sa.un, j) == -ENAMETOOLONG); /* too long for AF_UNIX socket */ + ASSERT_NOT_NULL(j = path_join(t, "sock")); + ASSERT_ERROR(sockaddr_un_set_path(&sa.un, j), ENAMETOOLONG); /* too long for AF_UNIX socket */ - assert_se(asprintf(&sh, "/tmp/%" PRIx64, random_u64()) >= 0); - assert_se(symlink(t, sh) >= 0); /* create temporary symlink, to access it anyway */ + ASSERT_OK_ERRNO(asprintf(&sh, "/tmp/%" PRIx64, random_u64())); + ASSERT_OK_ERRNO(symlink(t, sh)); /* create temporary symlink, to access it anyway */ free(j); - assert_se(j = path_join(sh, "sock")); - assert_se(sockaddr_un_set_path(&sa.un, j) >= 0); + ASSERT_NOT_NULL(j = path_join(sh, "sock")); + ASSERT_OK(sockaddr_un_set_path(&sa.un, j)); - fd1 = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); - assert_se(fd1 >= 0); - assert_se(bind(fd1, &sa.sa, sockaddr_len(&sa)) >= 0); - assert_se(listen(fd1, 1) >= 0); + ASSERT_OK_ERRNO(fd1 = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0)); + ASSERT_OK_ERRNO(bind(fd1, &sa.sa, sockaddr_len(&sa))); + ASSERT_OK_ERRNO(listen(fd1, 1)); sh = unlink_and_free(sh); /* remove temporary symlink */ - fd2 = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); - assert_se(fd2 >= 0); - assert_se(connect(fd2, &sa.sa, sockaddr_len(&sa)) < 0); - assert_se(errno == ENOENT); /* we removed the symlink, must fail */ + ASSERT_OK_ERRNO(fd2 = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0)); + ASSERT_ERROR_ERRNO(connect(fd2, &sa.sa, sockaddr_len(&sa)), ENOENT); /* we removed the symlink, must fail */ free(j); - assert_se(j = path_join(t, "sock")); + ASSERT_NOT_NULL(j = path_join(t, "sock")); - fd3 = open(j, O_CLOEXEC|O_PATH|O_NOFOLLOW); - assert_se(fd3 > 0); - assert_se(sockaddr_un_set_path(&sa.un, FORMAT_PROC_FD_PATH(fd3)) >= 0); /* connect via O_PATH instead, circumventing 108ch limit */ + ASSERT_OK_ERRNO(fd3 = open(j, O_CLOEXEC|O_PATH|O_NOFOLLOW)); + ASSERT_OK(sockaddr_un_set_path(&sa.un, FORMAT_PROC_FD_PATH(fd3))); /* connect via O_PATH instead, circumventing 108ch limit */ - assert_se(connect(fd2, &sa.sa, sockaddr_len(&sa)) >= 0); + ASSERT_OK_ERRNO(connect(fd2, &sa.sa, sockaddr_len(&sa))); } TEST(getpeerpidref) { From ea21c6d75ffbd5a41d7d9b6cfba3bf25d753a579 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 30 Jun 2026 21:46:57 +0200 Subject: [PATCH 48/71] sysupdate: drop redundant () Just a tiny simplification --- src/sysupdate/sysupdate.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index e5af39849c573..87c1cc6d4292e 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -2155,11 +2155,12 @@ static int context_list_components(Context *context, char ***ret_component_names /* Does the system have at least one transfer file in /etc/sysupdate.d, which can be considered a * TARGET_HOST? See target_get_argument() in sysupdated.c */ if (ret_has_default_component) - *ret_has_default_component = (!context->definitions && - !context->component && - !context->root && - !context->image && - context->n_transfers > 0); + *ret_has_default_component = + !context->definitions && + !context->component && + !context->root && + !context->image && + context->n_transfers > 0; return 0; } From 5275695271f5d777991c097be80307e474f2728e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 3 Jul 2026 15:35:48 +0200 Subject: [PATCH 49/71] sysupdate: replace local specifier_table[] by common system_and_tmp_specifier_table[] It's byte-by-byte exactly the same thing, hence drop the local copy. --- src/sysupdate/sysupdate-feature.c | 4 ++-- src/sysupdate/sysupdate-transfer.c | 12 ++++++------ src/sysupdate/sysupdate.c | 7 ------- src/sysupdate/sysupdate.h | 3 --- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/sysupdate/sysupdate-feature.c b/src/sysupdate/sysupdate-feature.c index 0c9bb112fe9b7..45a913f7c16a3 100644 --- a/src/sysupdate/sysupdate-feature.c +++ b/src/sysupdate/sysupdate-feature.c @@ -4,8 +4,8 @@ #include "conf-parser.h" #include "hash-funcs.h" #include "path-util.h" +#include "specifier.h" #include "string-util.h" -#include "sysupdate.h" #include "sysupdate-feature.h" #include "web-util.h" @@ -65,7 +65,7 @@ static int config_parse_url_specifiers( return 0; } - r = specifier_printf(rvalue, NAME_MAX, specifier_table, root, NULL, &resolved); + r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, root, NULL, &resolved); if (r < 0) { log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue); diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index c79743cad6908..08396d61b1c0c 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -127,7 +127,7 @@ static int config_parse_protect_version( assert(rvalue); - r = specifier_printf(rvalue, NAME_MAX, specifier_table, t->context->root, NULL, &resolved); + r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved); if (r < 0) { log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to expand specifiers in ProtectVersion=, ignoring: %s", rvalue); @@ -166,7 +166,7 @@ static int config_parse_min_version( assert(rvalue); - r = specifier_printf(rvalue, NAME_MAX, specifier_table, t->context->root, NULL, &resolved); + r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved); if (r < 0) { log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to expand specifiers in MinVersion=, ignoring: %s", rvalue); @@ -205,7 +205,7 @@ static int config_parse_url_specifiers( return 0; } - r = specifier_printf(rvalue, NAME_MAX, specifier_table, t->context->root, NULL, &resolved); + r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved); if (r < 0) { log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue); @@ -244,7 +244,7 @@ static int config_parse_current_symlink( assert(rvalue); - r = specifier_printf(rvalue, NAME_MAX, specifier_table, t->context->root, NULL, &resolved); + r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved); if (r < 0) { log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to expand specifiers in CurrentSymlink=, ignoring: %s", rvalue); @@ -333,7 +333,7 @@ static int config_parse_resource_pattern( if (r == 0) break; - r = specifier_printf(word, NAME_MAX, specifier_table, t->context->root, NULL, &resolved); + r = specifier_printf(word, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved); if (r < 0) { log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to expand specifiers in MatchPattern=, ignoring: %s", rvalue); @@ -377,7 +377,7 @@ static int config_parse_resource_path( return 0; } - r = specifier_printf(rvalue, PATH_MAX-1, specifier_table, t->context->root, NULL, &resolved); + r = specifier_printf(rvalue, PATH_MAX-1, system_and_tmp_specifier_table, t->context->root, NULL, &resolved); if (r < 0) { log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to expand specifiers in Path=, ignoring: %s", rvalue); diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 87c1cc6d4292e..0a805378cf29a 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -34,7 +34,6 @@ #include "pretty-print.h" #include "runtime-scope.h" #include "sort-util.h" -#include "specifier.h" #include "string-util.h" #include "strv.h" #include "sysupdate.h" @@ -74,12 +73,6 @@ STATIC_DESTRUCTOR_REGISTER(arg_component, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep); -const Specifier specifier_table[] = { - COMMON_SYSTEM_SPECIFIERS, - COMMON_TMP_SPECIFIERS, - {} -}; - #define CONTEXT_NULL \ (Context) { \ .sync = true, \ diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h index 02a9f7c8f348c..3dcaae527d196 100644 --- a/src/sysupdate/sysupdate.h +++ b/src/sysupdate/sysupdate.h @@ -1,7 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once -#include "specifier.h" #include "sysupdate-forward.h" #include "sysupdate-target.h" @@ -46,5 +45,3 @@ typedef struct Context { } Context; void context_done(Context *c); - -extern const Specifier specifier_table[]; From 922849aa7e14b0cc8d50c834f65403b2d174f487 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 3 Jul 2026 15:41:42 +0200 Subject: [PATCH 50/71] sysupdate: align macro backslashes like we usually do --- src/sysupdate/sysupdate.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 0a805378cf29a..099ec18c6a214 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -73,13 +73,13 @@ STATIC_DESTRUCTOR_REGISTER(arg_component, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep); -#define CONTEXT_NULL \ - (Context) { \ - .sync = true, \ - .instances_max = UINT64_MAX, \ - .verify = -1, \ - .cleanup = -1, \ - .installdb_fd = -EBADF, \ +#define CONTEXT_NULL \ + (Context) { \ + .sync = true, \ + .instances_max = UINT64_MAX, \ + .verify = -1, \ + .cleanup = -1, \ + .installdb_fd = -EBADF, \ .target_identifier.class = _TARGET_CLASS_INVALID, \ } From 8a502c4a769ba19d4682b477f0aee5cb1b1e3099 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 18 Jun 2026 17:30:35 +0200 Subject: [PATCH 51/71] sysupdate: split out reading of feature files into separate helper This is preparation for later adding a per-component metadata file, so that we can have nicely symmetric functions for this. --- src/sysupdate/sysupdate.c | 72 ++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 099ec18c6a214..789a9e48e93fc 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -179,6 +179,49 @@ static void server_done(Server *s) { static DEFINE_POINTER_ARRAY_FREE_FUNC(Transfer*, transfer_free); +static int read_features( + Context *c, + const char **dirs) { + + int r; + + assert(c); + + ConfFile **files = NULL; + size_t n_files = 0; + CLEANUP_ARRAY(files, n_files, conf_file_free_array); + + r = conf_files_list_strv_full( + ".feature", + c->root, + CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN, + dirs, + &files, + &n_files); + if (r < 0) + return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m"); + + FOREACH_ARRAY(i, files, n_files) { + ConfFile *e = *i; + + _cleanup_(feature_unrefp) Feature *f = feature_new(); + if (!f) + return log_oom(); + + r = feature_read_definition(f, c->root, e->result, dirs); + if (r < 0) + return r; + + r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f); + if (r < 0) + return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id); + + TAKE_PTR(f); + } + + return 0; +} + static int read_definitions( Context *c, const char **dirs, @@ -272,34 +315,9 @@ static int context_read_definitions(Context *c, const char* node, ReadDefinition if (!dirs) return log_oom(); - ConfFile **files = NULL; - size_t n_files = 0; - - CLEANUP_ARRAY(files, n_files, conf_file_free_array); - - r = conf_files_list_strv_full(".feature", c->root, - CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN, - (const char**) dirs, &files, &n_files); + r = read_features(c, (const char**) dirs); if (r < 0) - return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m"); - - FOREACH_ARRAY(i, files, n_files) { - _cleanup_(feature_unrefp) Feature *f = NULL; - ConfFile *e = *i; - - f = feature_new(); - if (!f) - return log_oom(); - - r = feature_read_definition(f, c->root, e->result, (const char**) dirs); - if (r < 0) - return r; - - r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f); - if (r < 0) - return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id); - TAKE_PTR(f); - } + return r; r = read_definitions(c, (const char**) dirs, ".transfer", node); if (r < 0) From 55127751ccc841633268de1b952226be056ddaf3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 18 Jun 2026 17:33:09 +0200 Subject: [PATCH 52/71] =?UTF-8?q?sysupdate:=20rename=20read=5Fdefinitions(?= =?UTF-8?q?)=20=E2=86=92=20read=5Ftransfers()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This function only reads the .transfer files, hence give it a precise name. --- src/sysupdate/sysupdate.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 789a9e48e93fc..2a277026b007c 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -222,7 +222,7 @@ static int read_features( return 0; } -static int read_definitions( +static int read_transfers( Context *c, const char **dirs, const char *suffix, @@ -319,13 +319,13 @@ static int context_read_definitions(Context *c, const char* node, ReadDefinition if (r < 0) return r; - r = read_definitions(c, (const char**) dirs, ".transfer", node); + r = read_transfers(c, (const char**) dirs, ".transfer", node); if (r < 0) return r; if (c->n_transfers + c->n_disabled_transfers == 0) { /* Backwards-compat: If no .transfer defs are found, fall back to trying .conf! */ - r = read_definitions(c, (const char**) dirs, ".conf", node); + r = read_transfers(c, (const char**) dirs, ".conf", node); if (r < 0) return r; From 138829b7843c6f744764d72d2c995005bfe6dc00 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 18 Jun 2026 17:58:09 +0200 Subject: [PATCH 53/71] sysupdate: split out url config parser into new file These are located at two distinct places, and are very similar, let's put them in a common place. This is preparation for reusing them later. --- src/sysupdate/meson.build | 1 + src/sysupdate/sysupdate-config.c | 92 ++++++++++++++++++++++++++++++ src/sysupdate/sysupdate-config.h | 7 +++ src/sysupdate/sysupdate-feature.c | 42 +------------- src/sysupdate/sysupdate-transfer.c | 88 +++++++++++----------------- 5 files changed, 134 insertions(+), 96 deletions(-) create mode 100644 src/sysupdate/sysupdate-config.c create mode 100644 src/sysupdate/sysupdate-config.h diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build index b6f9436606568..ec873269047e8 100644 --- a/src/sysupdate/meson.build +++ b/src/sysupdate/meson.build @@ -3,6 +3,7 @@ systemd_sysupdate_sources = files( 'sysupdate-cache.c', 'sysupdate-cleanup.c', + 'sysupdate-config.c', 'sysupdate-feature.c', 'sysupdate-instance.c', 'sysupdate-partition.c', diff --git a/src/sysupdate/sysupdate-config.c b/src/sysupdate/sysupdate-config.c new file mode 100644 index 0000000000000..7f553a95f0b41 --- /dev/null +++ b/src/sysupdate/sysupdate-config.c @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "log.h" +#include "specifier.h" +#include "string-util.h" +#include "strv.h" +#include "sysupdate-config.h" +#include "web-util.h" + +int config_parse_url_specifiers( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + const char *root = userdata; + char **s = ASSERT_PTR(data); + int r; + + assert(rvalue); + + if (isempty(rvalue)) { + *s = mfree(*s); + return 0; + } + + _cleanup_free_ char *resolved = NULL; + r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, root, NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue); + return 0; + } + + if (!http_url_is_valid(resolved)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "%s= URL is not valid, ignoring: %s", lvalue, rvalue); + return 0; + } + + return free_and_replace(*s, resolved); +} + +int config_parse_url_specifiers_many( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + const char *root = userdata; + char ***s = ASSERT_PTR(data); + int r; + + assert(rvalue); + + if (isempty(rvalue)) { + *s = strv_free(*s); + return 0; + } + + _cleanup_free_ char *resolved = NULL; + r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, root, NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue); + return 0; + } + + if (!http_url_is_valid(resolved)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "%s= URL is not valid, ignoring: %s", lvalue, rvalue); + return 0; + } + + if (strv_consume(s, TAKE_PTR(resolved)) < 0) + return log_oom(); + + return 0; +} diff --git a/src/sysupdate/sysupdate-config.h b/src/sysupdate/sysupdate-config.h new file mode 100644 index 0000000000000..bd972d598d56a --- /dev/null +++ b/src/sysupdate/sysupdate-config.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser-forward.h" + +CONFIG_PARSER_PROTOTYPE(config_parse_url_specifiers); +CONFIG_PARSER_PROTOTYPE(config_parse_url_specifiers_many); diff --git a/src/sysupdate/sysupdate-feature.c b/src/sysupdate/sysupdate-feature.c index 45a913f7c16a3..49c2e77988b93 100644 --- a/src/sysupdate/sysupdate-feature.c +++ b/src/sysupdate/sysupdate-feature.c @@ -4,10 +4,9 @@ #include "conf-parser.h" #include "hash-funcs.h" #include "path-util.h" -#include "specifier.h" #include "string-util.h" +#include "sysupdate-config.h" #include "sysupdate-feature.h" -#include "web-util.h" static Feature *feature_free(Feature *f) { if (!f) @@ -42,45 +41,6 @@ DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(feature_hash_ops, char, string_hash_func, string_compare_func, Feature, feature_unref); -static int config_parse_url_specifiers( - const char *unit, - const char *filename, - unsigned line, - const char *section, - unsigned section_line, - const char *lvalue, - int ltype, - const char *rvalue, - void *data, - void *userdata) { - char **s = ASSERT_PTR(data); - const char *root = ASSERT_PTR(userdata); - _cleanup_free_ char *resolved = NULL; - int r; - - assert(rvalue); - - if (isempty(rvalue)) { - *s = mfree(*s); - return 0; - } - - r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, root, NULL, &resolved); - if (r < 0) { - log_syntax(unit, LOG_WARNING, filename, line, r, - "Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue); - return 0; - } - - if (!http_url_is_valid(resolved)) { - log_syntax(unit, LOG_WARNING, filename, line, 0, - "%s= URL is not valid, ignoring: %s", lvalue, rvalue); - return 0; - } - - return free_and_replace(*s, resolved); -} - int feature_read_definition(Feature *f, const char *root, const char *path, const char *const *dirs) { assert(f); diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index 08396d61b1c0c..2a29448836aa7 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -36,6 +36,7 @@ #include "sync-util.h" #include "sysupdate.h" #include "sysupdate-cleanup.h" +#include "sysupdate-config.h" #include "sysupdate-feature.h" #include "sysupdate-instance.h" #include "sysupdate-pattern.h" @@ -182,7 +183,7 @@ static int config_parse_min_version( return free_and_replace(*version, resolved); } -static int config_parse_url_specifiers( +static int config_parse_transfer_url_specifiers_many( const char *unit, const char *filename, unsigned line, @@ -193,36 +194,13 @@ static int config_parse_url_specifiers( const char *rvalue, void *data, void *userdata) { - char ***s = ASSERT_PTR(data); - Transfer *t = ASSERT_PTR(userdata); - _cleanup_free_ char *resolved = NULL; - int r; - - assert(rvalue); - - if (isempty(rvalue)) { - *s = strv_free(*s); - return 0; - } - - r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved); - if (r < 0) { - log_syntax(unit, LOG_WARNING, filename, line, r, - "Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue); - return 0; - } - if (!http_url_is_valid(resolved)) { - log_syntax(unit, LOG_WARNING, filename, line, 0, - "%s= URL is not valid, ignoring: %s", lvalue, rvalue); - return 0; - } + Transfer *t = ASSERT_PTR(userdata); - r = strv_push(s, TAKE_PTR(resolved)); - if (r < 0) - return log_oom(); + /* Here we expect userdata to point to our Transfer object, but config_parse_url_specifiers() wants + * the root dir there, hence fix this up */ - return 0; + return config_parse_url_specifiers_many(unit, filename, line, section, section_line, lvalue, ltype, rvalue, data, t->context->root); } static int config_parse_current_symlink( @@ -510,33 +488,33 @@ int transfer_read_definition(Transfer *t, const char *path, const char **dirs, H assert(t); ConfigTableItem table[] = { - { "Transfer", "MinVersion", config_parse_min_version, 0, &t->min_version }, - { "Transfer", "ProtectVersion", config_parse_protect_version, 0, &t->protected_versions }, - { "Transfer", "Verify", config_parse_bool, 0, &t->verify }, - { "Transfer", "ChangeLog", config_parse_url_specifiers, 0, &t->changelog }, - { "Transfer", "AppStream", config_parse_url_specifiers, 0, &t->appstream }, - { "Transfer", "Features", config_parse_strv, 0, &t->features }, - { "Transfer", "RequisiteFeatures", config_parse_strv, 0, &t->requisite_features }, - { "Source", "Type", config_parse_resource_type, 0, &t->source.type }, - { "Source", "Path", config_parse_resource_path, 0, &t->source }, - { "Source", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->source.path_relative_to }, - { "Source", "MatchPattern", config_parse_resource_pattern, 0, &t->source.patterns }, - { "Target", "Type", config_parse_resource_type, 0, &t->target.type }, - { "Target", "Path", config_parse_resource_path, 0, &t->target }, - { "Target", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->target.path_relative_to }, - { "Target", "MatchPattern", config_parse_resource_pattern, 0, &t->target.patterns }, - { "Target", "MatchPartitionType", config_parse_resource_ptype, 0, &t->target }, - { "Target", "PartitionUUID", config_parse_partition_uuid, 0, t }, - { "Target", "PartitionFlags", config_parse_partition_flags, 0, t }, - { "Target", "PartitionNoAuto", config_parse_tristate, 0, &t->no_auto }, - { "Target", "PartitionGrowFileSystem", config_parse_tristate, 0, &t->growfs }, - { "Target", "ReadOnly", config_parse_tristate, 0, &t->read_only }, - { "Target", "Mode", config_parse_mode, 0, &t->mode }, - { "Target", "TriesLeft", config_parse_uint64, 0, &t->tries_left }, - { "Target", "TriesDone", config_parse_uint64, 0, &t->tries_done }, - { "Target", "InstancesMax", config_parse_instances_max, 0, &t->instances_max }, - { "Target", "RemoveTemporary", config_parse_bool, 0, &t->remove_temporary }, - { "Target", "CurrentSymlink", config_parse_current_symlink, 0, &t->current_symlink }, + { "Transfer", "MinVersion", config_parse_min_version, 0, &t->min_version }, + { "Transfer", "ProtectVersion", config_parse_protect_version, 0, &t->protected_versions }, + { "Transfer", "Verify", config_parse_bool, 0, &t->verify }, + { "Transfer", "ChangeLog", config_parse_transfer_url_specifiers_many, 0, &t->changelog }, + { "Transfer", "AppStream", config_parse_transfer_url_specifiers_many, 0, &t->appstream }, + { "Transfer", "Features", config_parse_strv, 0, &t->features }, + { "Transfer", "RequisiteFeatures", config_parse_strv, 0, &t->requisite_features }, + { "Source", "Type", config_parse_resource_type, 0, &t->source.type }, + { "Source", "Path", config_parse_resource_path, 0, &t->source }, + { "Source", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->source.path_relative_to }, + { "Source", "MatchPattern", config_parse_resource_pattern, 0, &t->source.patterns }, + { "Target", "Type", config_parse_resource_type, 0, &t->target.type }, + { "Target", "Path", config_parse_resource_path, 0, &t->target }, + { "Target", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->target.path_relative_to }, + { "Target", "MatchPattern", config_parse_resource_pattern, 0, &t->target.patterns }, + { "Target", "MatchPartitionType", config_parse_resource_ptype, 0, &t->target }, + { "Target", "PartitionUUID", config_parse_partition_uuid, 0, t }, + { "Target", "PartitionFlags", config_parse_partition_flags, 0, t }, + { "Target", "PartitionNoAuto", config_parse_tristate, 0, &t->no_auto }, + { "Target", "PartitionGrowFileSystem", config_parse_tristate, 0, &t->growfs }, + { "Target", "ReadOnly", config_parse_tristate, 0, &t->read_only }, + { "Target", "Mode", config_parse_mode, 0, &t->mode }, + { "Target", "TriesLeft", config_parse_uint64, 0, &t->tries_left }, + { "Target", "TriesDone", config_parse_uint64, 0, &t->tries_done }, + { "Target", "InstancesMax", config_parse_instances_max, 0, &t->instances_max }, + { "Target", "RemoveTemporary", config_parse_bool, 0, &t->remove_temporary }, + { "Target", "CurrentSymlink", config_parse_current_symlink, 0, &t->current_symlink }, {} }; From 3bcd707d5650fdf91de19cf6e1ca5bbac7686967 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 17:21:52 +0100 Subject: [PATCH 54/71] boot: make device_path_next_node() robust against malformed zero-length nodes device_path_next_node() advances by the current node's Length field, which per the EFI device path protocol includes the 4-byte node header; a well-formed node is therefore at least sizeof(EFI_DEVICE_PATH) bytes long. A malformed node with Length < sizeof(EFI_DEVICE_PATH), in particular Length == 0, makes the helper return its input pointer unchanged. Advance by at least sizeof(EFI_DEVICE_PATH). Follow-up for 5080a60a719da213fa90964b76cc90bd0d1cb8de --- src/boot/device-path-util.c | 24 ++++++++++++++++++++++++ src/boot/device-path-util.h | 7 +++++++ 2 files changed, 31 insertions(+) diff --git a/src/boot/device-path-util.c b/src/boot/device-path-util.c index 5243e81e5df18..73c3933299c07 100644 --- a/src/boot/device-path-util.c +++ b/src/boot/device-path-util.c @@ -25,6 +25,10 @@ EFI_STATUS make_file_device_path(EFI_HANDLE device, const char16_t *file, EFI_DE const EFI_DEVICE_PATH *end_node = device_path_find_end_node(dp); size_t file_size = strsize16(file); + /* The node Length is a uint16_t, so refuse a path that would not fit. */ + if (file_size > UINT16_MAX - sizeof(FILEPATH_DEVICE_PATH)) + return EFI_INVALID_PARAMETER; + size_t dp_size = (uint8_t *) end_node - (uint8_t *) dp; /* Make a copy that can also hold a file media device path. */ @@ -55,6 +59,9 @@ EFI_STATUS make_url_device_path(const char16_t *url, EFI_DEVICE_PATH **ret) { return EFI_INVALID_PARAMETER; size_t l = strlen8(u); + /* The node Length is a uint16_t, so refuse a URL that would not fit. */ + if (l > UINT16_MAX - offsetof(URI_DEVICE_PATH, Uri)) + return EFI_INVALID_PARAMETER; size_t t = offsetof(URI_DEVICE_PATH, Uri) + l + sizeof(EFI_DEVICE_PATH); EFI_DEVICE_PATH *dp = xmalloc(t); @@ -177,6 +184,23 @@ EFI_DEVICE_PATH *device_path_replace_node( return ret; } +bool device_path_is_valid(const EFI_DEVICE_PATH *dp, size_t size) { + if (!dp) + return false; + + /* Validate against the known size so a truncated/corrupt path can't make us read out of bounds. */ + for (;;) { + if (size < sizeof(EFI_DEVICE_PATH)) + return false; + if (dp->Length < sizeof(EFI_DEVICE_PATH) || dp->Length > size) + return false; + if (device_path_is_end(dp)) + return true; + size -= dp->Length; + dp = (const EFI_DEVICE_PATH *) ((const uint8_t *) dp + dp->Length); + } +} + size_t device_path_size(const EFI_DEVICE_PATH *dp) { const EFI_DEVICE_PATH *i = ASSERT_PTR(dp); diff --git a/src/boot/device-path-util.h b/src/boot/device-path-util.h index b02cf3146a9ec..8a993f27fe909 100644 --- a/src/boot/device-path-util.h +++ b/src/boot/device-path-util.h @@ -13,6 +13,9 @@ EFI_DEVICE_PATH *device_path_replace_node( static inline EFI_DEVICE_PATH *device_path_next_node(const EFI_DEVICE_PATH *dp) { assert(dp); + /* The node Length includes the 4-byte header, so a well-formed node is at least that long. Paths + * coming from untrusted sources must be checked with device_path_is_valid() before being walked. */ + assert(dp->Length >= sizeof(EFI_DEVICE_PATH)); return (EFI_DEVICE_PATH *) ((uint8_t *) dp + dp->Length); } @@ -30,4 +33,8 @@ static inline bool device_path_is_end(const EFI_DEVICE_PATH *dp) { size_t device_path_size(const EFI_DEVICE_PATH *dp); +/* Validates that a device path is well-formed and fully contained within the given size, terminated by an + * end node. Use on paths from untrusted sources (e.g. EFI variables) before walking them. */ +bool device_path_is_valid(const EFI_DEVICE_PATH *dp, size_t size); + EFI_DEVICE_PATH *device_path_dup(const EFI_DEVICE_PATH *dp); From 7b7599b72d84a948301107b41ae82256e8feb5bb Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 17:29:31 +0100 Subject: [PATCH 55/71] boot: initialize return parameters on zero-length EFI variable read When a variable exists but is empty, the initial size-query GetVariable() in efivar_get_raw_full() returns EFI_SUCCESS instead of EFI_BUFFER_TOO_SMALL: the zero-length payload already "fits" the zero-length query buffer. The helper returns success, but does not initialize the return parameters. Handle a couple of corner cases by checking the return size. Follow-up for a40960748907212883f4b7de7367e6870657016e --- src/boot/efi-efivars.c | 13 +++++++++++++ src/boot/vmm.c | 16 +++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/boot/efi-efivars.c b/src/boot/efi-efivars.c index c1ae4252c3c34..6af325966f965 100644 --- a/src/boot/efi-efivars.c +++ b/src/boot/efi-efivars.c @@ -191,6 +191,16 @@ EFI_STATUS efivar_get_raw_full( size_t size = 0; err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &size, NULL); + if (err == EFI_SUCCESS) { + /* The variable exists but is empty, initialize return parameters */ + if (ret_attributes) + *ret_attributes = 0; + if (ret_data) + *ret_data = NULL; + if (ret_size) + *ret_size = 0; + return EFI_SUCCESS; + } if (err != EFI_BUFFER_TOO_SMALL) return err; @@ -222,6 +232,9 @@ EFI_STATUS efivar_get_boolean_u8(const EFI_GUID *vendor, const char16_t *name, b if (err != EFI_SUCCESS) return err; + if (size == 0) + return EFI_BUFFER_TOO_SMALL; + if (ret) *ret = *b > 0; diff --git a/src/boot/vmm.c b/src/boot/vmm.c index e571a3990de64..09c121b9884f0 100644 --- a/src/boot/vmm.c +++ b/src/boot/vmm.c @@ -62,7 +62,7 @@ bool is_direct_boot(EFI_HANDLE device) { EFI_STATUS vmm_open(EFI_HANDLE *ret_vmm_dev, EFI_FILE **ret_vmm_dir) { _cleanup_free_ EFI_HANDLE *handles = NULL; size_t n_handles; - EFI_STATUS err, dp_err; + EFI_STATUS err; assert(ret_vmm_dev); assert(ret_vmm_dir); @@ -79,9 +79,15 @@ EFI_STATUS vmm_open(EFI_HANDLE *ret_vmm_dev, EFI_FILE **ret_vmm_dir) { for (size_t order = 0;; order++) { _cleanup_free_ EFI_DEVICE_PATH *dp = NULL; + size_t dp_size = 0; _cleanup_free_ char16_t *order_str = xasprintf("VMMBootOrder%04zx", order); - dp_err = efivar_get_raw(MAKE_GUID_PTR(VMM_BOOT_ORDER), order_str, (void**) &dp, NULL); + err = efivar_get_raw(MAKE_GUID_PTR(VMM_BOOT_ORDER), order_str, (void**) &dp, &dp_size); + + /* Drop the device path from the (untrusted) EFI variable if it doesn't validate, so the + * check below simply has to test whether it is set. */ + if (err == EFI_SUCCESS && !device_path_is_valid(dp, dp_size)) + dp = mfree(dp); for (size_t i = 0; i < n_handles; i++) { _cleanup_file_close_ EFI_FILE *root_dir = NULL, *efi_dir = NULL; @@ -92,8 +98,8 @@ EFI_STATUS vmm_open(EFI_HANDLE *ret_vmm_dev, EFI_FILE **ret_vmm_dir) { if (err != EFI_SUCCESS) return err; - /* check against VMMBootOrderNNNN (if set) */ - if (dp_err == EFI_SUCCESS && !device_path_startswith(fs, dp)) + /* check against VMMBootOrderNNNN (if set and valid) */ + if (dp && !device_path_startswith(fs, dp)) continue; err = open_volume(handles[i], &root_dir); @@ -112,7 +118,7 @@ EFI_STATUS vmm_open(EFI_HANDLE *ret_vmm_dev, EFI_FILE **ret_vmm_dir) { return EFI_SUCCESS; } - if (dp_err != EFI_SUCCESS) + if (!dp) return EFI_NOT_FOUND; } assert_not_reached(); From 7378b51f66e8e7e87aa026b2ba2c7c785a0a509d Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 19:41:31 +0100 Subject: [PATCH 56/71] boot: don't unquote an empty value in line_get_key_value() de0da85d41b switched the unquote check to strchr8(QUOTES, value[0]), which is not equivalent to the old explicit comparison for an empty value: strchr8(), like strchr(3), returns a pointer to the haystack's terminating NUL when the needle is '\0', so strchr8(QUOTES, '\0') is non-NULL. For a line whose separator is the last byte (e.g. "ID=") the split leaves value[0] == '\0' and line[linelen - 1] == '\0' too, so both conjuncts hold and value++ steps one byte past the value's terminator. Follow-up for de0da85d41b207b850aa0f68bb2436525389cf2b --- src/boot/efi-string.c | 2 +- src/boot/test-efi-string.c | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/boot/efi-string.c b/src/boot/efi-string.c index 306446386271a..efe1553696ddf 100644 --- a/src/boot/efi-string.c +++ b/src/boot/efi-string.c @@ -500,7 +500,7 @@ char* line_get_key_value(char *s, const char *sep, size_t *pos, char **ret_key, value++; /* unquote */ - if (strchr8(QUOTES, value[0]) && line[linelen - 1] == value[0]) { + if (value[0] != '\0' && strchr8(QUOTES, value[0]) && line[linelen - 1] == value[0]) { value++; line[linelen - 1] = '\0'; } diff --git a/src/boot/test-efi-string.c b/src/boot/test-efi-string.c index 7633534dd4c07..5805cb302412d 100644 --- a/src/boot/test-efi-string.c +++ b/src/boot/test-efi-string.c @@ -590,6 +590,7 @@ TEST(line_get_key_value) { " also\tused \r\n" "for \"the conf\"\n" "format\t !!"; + char s3[] = "ID="; size_t pos = 0; char *key, *value; @@ -611,6 +612,11 @@ TEST(line_get_key_value) { ASSERT_TRUE(streq8(value, " stripping # with comments")); ASSERT_NULL(line_get_key_value(s1, "=", &pos, &key, &value)); + pos = 0; + ASSERT_NOT_NULL(line_get_key_value(s3, "=", &pos, &key, &value)); + ASSERT_TRUE(streq8(key, "ID")); + ASSERT_TRUE(streq8(value, "")); + pos = 0; ASSERT_NOT_NULL(line_get_key_value(s2, " \t", &pos, &key, &value)); ASSERT_TRUE(streq8(key, "this")); From ecf3f5056a74058fc60de91a7784ab01ce27dec8 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 19:57:23 +0100 Subject: [PATCH 57/71] boot: reject inner kernel entry point outside the image pe_kernel_info() returned AddressOfEntryPoint (and the .compat section entry_point) straight from the PE header with no check against SizeOfImage. Since cab9c7b5a4 the stub calls the inner kernel directly as ImageBase + entry_point, and only EFI_SIZE_TO_PAGES(SizeOfImage) pages are allocated for it. Follow-up for cab9c7b5a42effa8a45611fc6b8556138c869b5f --- src/boot/pe.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/boot/pe.c b/src/boot/pe.c index 872cb9e6220e4..685812a488a3e 100644 --- a/src/boot/pe.c +++ b/src/boot/pe.c @@ -520,6 +520,10 @@ EFI_STATUS pe_kernel_info( return EFI_UNSUPPORTED; if (pe->FileHeader.Machine == TARGET_MACHINE_TYPE) { + /* The entry point is later called as ImageBase + entry_point, and only SizeOfImage + * bytes are allocated for the image, so reject an entry point outside of it. */ + if (pe->OptionalHeader.AddressOfEntryPoint >= size_in_memory) + return EFI_LOAD_ERROR; if (ret_entry_point) *ret_entry_point = pe->OptionalHeader.AddressOfEntryPoint; if (ret_compat_entry_point) @@ -535,6 +539,9 @@ EFI_STATUS pe_kernel_info( if (compat_entry_point == 0) /* Image type not supported and no compat entry found. */ return EFI_UNSUPPORTED; + if (compat_entry_point >= size_in_memory) + /* Same as above: the compat entry point is called as ImageBase + entry_point. */ + return EFI_LOAD_ERROR; if (ret_entry_point) *ret_entry_point = 0; From feeba8fa3b77ae3e8f0c2bedda94e4dab23d85c7 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 20:12:36 +0100 Subject: [PATCH 58/71] boot: bound PE section VirtualSize before zeroing the inner kernel The inner-kernel section loader checks VirtualAddress + SizeOfRawData against kernel_size_in_memory (for the memcpy), but the memzero right after it clears up to VirtualAddress + VirtualSize, and VirtualSize is only constrained to be >= SizeOfRawData. Reject a VirtualAddress + VirtualSize that overflows or exceeds kernel_size_in_memory, mirroring the existing SizeOfRawData checks. Follow-up for cab9c7b5a42effa8a45611fc6b8556138c869b5f --- src/boot/linux.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/boot/linux.c b/src/boot/linux.c index bd71ada48358c..d1da46ed4afd3 100644 --- a/src/boot/linux.c +++ b/src/boot/linux.c @@ -289,6 +289,10 @@ EFI_STATUS linux_exec( return log_error_status(EFI_LOAD_ERROR, "Section would write outside of memory"); if (h->SizeOfRawData > h->VirtualSize) return log_error_status(EFI_LOAD_ERROR, "Invalid PE section, raw data size is greater than virtual size"); + if (UINT32_MAX - h->VirtualAddress < h->VirtualSize) + return log_error_status(EFI_LOAD_ERROR, "Invalid PE section, VirtualSize + VirtualAddress overflows"); + if (h->VirtualAddress + h->VirtualSize > kernel_size_in_memory) + return log_error_status(EFI_LOAD_ERROR, "Section virtual size would write outside of memory"); if (UINT32_MAX - h->PointerToRawData < h->SizeOfRawData) return log_error_status(EFI_LOAD_ERROR, "Invalid PE section, PointerToRawData + SizeOfRawData overflows"); if (h->PointerToRawData + h->SizeOfRawData > kernel->iov_len) From dc55e4a5a00539f39befca4b0350c5e70b067702 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 21:58:57 +0100 Subject: [PATCH 59/71] boot: check PE section against SizeOfImage pe_locate_sections_internal() stores each matching section's VirtualSize and VirtualAddress into PeSectionVector.memory_size/memory_offset with only SIZE_MAX overflow guards, never checking them against the image's SizeOfImage. Wire up the image's SizeOfImage down to pe_locate_sections_internal() and skip any section whose in-memory section does not fit within it. Follow-up for fb974ac485c90f9887d5d21ac25d6d26d452eb3c --- src/boot/boot.c | 13 +++-- src/boot/linux.c | 5 +- src/boot/pe.c | 126 +++++++++++++++++++++++++++++++++++++++-------- src/boot/pe.h | 10 +++- src/boot/stub.c | 8 +-- 5 files changed, 129 insertions(+), 33 deletions(-) diff --git a/src/boot/boot.c b/src/boot/boot.c index dbe7f0c8ab60c..c2f7e9b5008de 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -2046,8 +2046,8 @@ static bool is_sd_boot(EFI_FILE *root_dir, const char16_t *loader_path) { return false; _cleanup_free_ PeSectionHeader *section_table = NULL; - size_t n_section_table; - err = pe_section_table_from_file(handle, §ion_table, &n_section_table); + size_t n_section_table, size_in_memory; + err = pe_section_table_from_file(handle, §ion_table, &n_section_table, &size_in_memory); if (err != EFI_SUCCESS) return false; @@ -2058,6 +2058,7 @@ static bool is_sd_boot(EFI_FILE *root_dir, const char16_t *loader_path) { section_names, /* profile= */ UINT_MAX, /* validate_base= */ 0, + size_in_memory, vector); if (vector[0].memory_size != STRLEN(SD_MAGIC)) return false; @@ -2320,8 +2321,8 @@ static void boot_entry_add_type2( /* Load section table once */ _cleanup_free_ PeSectionHeader *section_table = NULL; - size_t n_section_table; - err = pe_section_table_from_file(handle, §ion_table, &n_section_table); + size_t n_section_table, size_in_memory; + err = pe_section_table_from_file(handle, §ion_table, &n_section_table, &size_in_memory); if (err != EFI_SUCCESS) return; @@ -2333,6 +2334,7 @@ static void boot_entry_add_type2( section_names, /* profile= */ UINT_MAX, /* validate_base= */ 0, + size_in_memory, base_sections); /* and now iterate through possible profiles, and create a menu item for each profile we find */ @@ -2348,6 +2350,7 @@ static void boot_entry_add_type2( section_names, profile, /* validate_base= */ 0, + size_in_memory, sections); if (err != EFI_SUCCESS && profile > 0) /* It's fine if there's no .profile for the first profile */ @@ -3100,7 +3103,7 @@ static EFI_STATUS call_image_start( if (err == EFI_UNSUPPORTED && entry->type == LOADER_LINUX) { uint32_t compat_address; - err = pe_kernel_info(loaded_image->ImageBase, /* ret_entry_point= */ NULL, &compat_address, + err = pe_kernel_info(loaded_image->ImageBase, loaded_image->ImageSize, /* ret_entry_point= */ NULL, &compat_address, /* ret_size_in_memory= */ NULL, /* ret_section_alignment= */ NULL); if (err != EFI_SUCCESS) { diff --git a/src/boot/linux.c b/src/boot/linux.c index d1da46ed4afd3..584fc051b71bf 100644 --- a/src/boot/linux.c +++ b/src/boot/linux.c @@ -167,7 +167,7 @@ EFI_STATUS linux_exec( assert(iovec_is_set(kernel)); assert(iovec_is_valid(initrd)); - err = pe_kernel_info(kernel->iov_base, &entry_point, &compat_entry_point, &kernel_size_in_memory, §ion_alignment); + err = pe_kernel_info(kernel->iov_base, kernel->iov_len, &entry_point, &compat_entry_point, &kernel_size_in_memory, §ion_alignment); #if defined(__i386__) || defined(__x86_64__) if (err == EFI_UNSUPPORTED) /* Kernel is too old to support LINUX_INITRD_MEDIA_GUID, try the deprecated EFI handover @@ -259,8 +259,7 @@ EFI_STATUS linux_exec( const PeSectionHeader *headers; size_t n_headers; - /* Do we need to validate anything here? the len? */ - err = pe_section_table_from_base(kernel->iov_base, &headers, &n_headers); + err = pe_section_table_from_base(kernel->iov_base, kernel->iov_len, &headers, &n_headers, /* ret_size_in_memory= */ NULL); if (err != EFI_SUCCESS) return log_error_status(err, "Cannot read sections: %m"); diff --git a/src/boot/pe.c b/src/boot/pe.c index 685812a488a3e..5e4c07e1610c8 100644 --- a/src/boot/pe.c +++ b/src/boot/pe.c @@ -173,6 +173,54 @@ static size_t section_table_offset(const DosFileHeader *dos, const PeFileHeader return dos->ExeHeader + offsetof(PeFileHeader, OptionalHeader) + pe->FileHeader.SizeOfOptionalHeader; } +static EFI_STATUS pe_headers_from_base( + const void *base, + size_t base_len, + bool allow_compatibility, + const DosFileHeader **ret_dos, + const PeFileHeader **ret_pe) { + + assert(base); + assert(ret_dos); + assert(ret_pe); + + /* Validate the DOS and PE headers against the buffer length */ + + if (base_len < sizeof(DosFileHeader)) + return EFI_LOAD_ERROR; + + const DosFileHeader *dos = (const DosFileHeader*) base; + if (!verify_dos(dos)) + return EFI_LOAD_ERROR; + + if (dos->ExeHeader > base_len || base_len - dos->ExeHeader < sizeof(PeFileHeader)) + return EFI_LOAD_ERROR; + + const PeFileHeader *pe = (const PeFileHeader*) ((const uint8_t*) base + dos->ExeHeader); + if (!verify_pe(dos, pe, allow_compatibility)) + return EFI_LOAD_ERROR; + + *ret_dos = dos; + *ret_pe = pe; + return EFI_SUCCESS; +} + +static bool pe_section_table_in_bounds( + const DosFileHeader *dos, + const PeFileHeader *pe, + size_t base_len) { + + assert(dos); + assert(pe); + + /* verify_pe() already bounded SizeOfOptionalHeader so this offset cannot overflow, and + * NumberOfSections is a uint16_t so the byte count cannot either. */ + size_t offset = section_table_offset(dos, pe); + size_t bytes = (size_t) pe->FileHeader.NumberOfSections * sizeof(PeSectionHeader); + + return offset <= base_len && base_len - offset >= bytes; +} + static bool pe_section_name_equal(const char *a, const char *b) { if (a == b) @@ -258,6 +306,7 @@ static void pe_locate_sections_internal( size_t n_section_table, const char *const section_names[], size_t validate_base, + size_t size_in_memory, const void *device_table, const Device *device, PeSectionVector sections[]) { @@ -288,6 +337,11 @@ static void pe_locate_sections_internal( if ((size_t) j->VirtualSize > size_max) continue; + /* The section's in-memory range must lie within the image, otherwise consumers + * reading it via memory_offset/memory_size would read past the loaded image. */ + if ((size_t) j->VirtualAddress + (size_t) j->VirtualSize > size_in_memory) + continue; + /* 2nd overflow check: ignore sections that are impossibly large also taking the * loaded base into account. */ if (validate_base != 0) { @@ -360,6 +414,7 @@ static void pe_locate_sections( size_t n_section_table, const char *const section_names[], size_t validate_base, + size_t size_in_memory, PeSectionVector sections[]) { if (!looking_for_dtbauto_or_efifw(section_names)) @@ -368,6 +423,7 @@ static void pe_locate_sections( n_section_table, section_names, validate_base, + size_in_memory, /* device_table= */ NULL, /* device= */ NULL, sections); @@ -387,6 +443,7 @@ static void pe_locate_sections( n_section_table, hwid_section_names, validate_base, + size_in_memory, /* device_table= */ NULL, /* device= */ NULL, hwids_section); @@ -403,6 +460,7 @@ static void pe_locate_sections( n_section_table, section_names, validate_base, + size_in_memory, hwids, device, sections); @@ -418,6 +476,7 @@ static void pe_locate_sections( n_section_table, section_names, validate_base, + size_in_memory, hwids, device, sections); @@ -433,12 +492,13 @@ static void pe_locate_sections( n_section_table, section_names, validate_base, + size_in_memory, hwids, device, sections); } -static uint32_t get_compatibility_entry_address(const DosFileHeader *dos, const PeFileHeader *pe) { +static uint32_t get_compatibility_entry_address(const DosFileHeader *dos, size_t base_len, const PeFileHeader *pe) { /* The kernel may provide alternative PE entry points for different PE architectures. This allows * booting a 64-bit kernel on 32-bit EFI that is otherwise running on a 64-bit CPU. The locations of any * such compat entry points are located in a special PE section. */ @@ -448,16 +508,29 @@ static uint32_t get_compatibility_entry_address(const DosFileHeader *dos, const static const char *const section_names[] = { ".compat", NULL }; PeSectionVector vector[1] = {}; + + /* Make sure the section table lies within the buffer before pe_locate_sections() iterates it. */ + if (!pe_section_table_in_bounds(dos, pe, base_len)) + return 0; + pe_locate_sections( (const PeSectionHeader *) ((const uint8_t *) dos + section_table_offset(dos, pe)), pe->FileHeader.NumberOfSections, section_names, PTR_TO_SIZE(dos), + pe->OptionalHeader.SizeOfImage, vector); if (!PE_SECTION_VECTOR_IS_SET(vector)) /* not found */ return 0; + /* pe_locate_sections() bounded the section against SizeOfImage, the in-memory size. Here we read the + * section data straight from the file buffer 'dos', which may be smaller than SizeOfImage, so also + * require the section's data range to lie within base_len before scanning it. */ + if (vector[0].memory_offset > base_len || + vector[0].memory_size > base_len - vector[0].memory_offset) + return 0; + typedef struct { uint8_t type; uint8_t size; @@ -487,19 +560,18 @@ static uint32_t get_compatibility_entry_address(const DosFileHeader *dos, const EFI_STATUS pe_kernel_info( const void *base, + size_t base_len, uint32_t *ret_entry_point, uint32_t *ret_compat_entry_point, size_t *ret_size_in_memory, uint32_t *ret_section_alignment) { assert(base); - const DosFileHeader *dos = (const DosFileHeader *) base; - if (!verify_dos(dos)) - return EFI_LOAD_ERROR; - - const PeFileHeader *pe = (const PeFileHeader *) ((const uint8_t *) base + dos->ExeHeader); - if (!verify_pe(dos, pe, /* allow_compatibility= */ true)) - return EFI_LOAD_ERROR; + const DosFileHeader *dos; + const PeFileHeader *pe; + EFI_STATUS err = pe_headers_from_base(base, base_len, /* allow_compatibility= */ true, &dos, &pe); + if (err != EFI_SUCCESS) + return err; /* When allocating we need to also consider the virtual/uninitialized data sections, so parse it out * of the SizeOfImage field in the PE header and return it */ @@ -535,7 +607,7 @@ EFI_STATUS pe_kernel_info( return EFI_SUCCESS; } - uint32_t compat_entry_point = get_compatibility_entry_address(dos, pe); + uint32_t compat_entry_point = get_compatibility_entry_address(dos, base_len, pe); if (compat_entry_point == 0) /* Image type not supported and no compat entry found. */ return EFI_UNSUPPORTED; @@ -606,20 +678,20 @@ bool pe_kernel_check_nx_compat(const void *base) { EFI_STATUS pe_section_table_from_base( const void *base, + size_t base_len, const PeSectionHeader **ret_section_table, - size_t *ret_n_section_table) { + size_t *ret_n_section_table, + size_t *ret_size_in_memory) { assert(base); assert(ret_section_table); assert(ret_n_section_table); - const DosFileHeader *dos = (const DosFileHeader*) base; - if (!verify_dos(dos)) - return EFI_LOAD_ERROR; - - const PeFileHeader *pe = (const PeFileHeader*) ((const uint8_t*) base + dos->ExeHeader); - if (!verify_pe(dos, pe, /* allow_compatibility= */ false)) - return EFI_LOAD_ERROR; + const DosFileHeader *dos; + const PeFileHeader *pe; + EFI_STATUS err = pe_headers_from_base(base, base_len, /* allow_compatibility= */ false, &dos, &pe); + if (err != EFI_SUCCESS) + return err; assert_cc(sizeof(pe->FileHeader.NumberOfSections) == sizeof(uint16_t)); /* multiplication below cannot overflow */ @@ -627,14 +699,22 @@ EFI_STATUS pe_section_table_from_base( if (n_section_table * sizeof(PeSectionHeader) > SECTION_TABLE_BYTES_MAX) return EFI_OUT_OF_RESOURCES; + /* Make sure the section table lies within the buffer, so consumers iterating it don't read off the + * end. */ + if (!pe_section_table_in_bounds(dos, pe, base_len)) + return EFI_LOAD_ERROR; + *ret_section_table = (const PeSectionHeader*) ((const uint8_t*) base + section_table_offset(dos, pe)); *ret_n_section_table = n_section_table; + if (ret_size_in_memory) + *ret_size_in_memory = pe->OptionalHeader.SizeOfImage; return EFI_SUCCESS; } EFI_STATUS pe_memory_locate_sections( const void *base, + size_t base_len, const char *const section_names[], PeSectionVector sections[]) { @@ -645,8 +725,8 @@ EFI_STATUS pe_memory_locate_sections( assert(sections); const PeSectionHeader *section_table; - size_t n_section_table; - err = pe_section_table_from_base(base, §ion_table, &n_section_table); + size_t n_section_table, size_in_memory; + err = pe_section_table_from_base(base, base_len, §ion_table, &n_section_table, &size_in_memory); if (err != EFI_SUCCESS) return err; @@ -655,6 +735,7 @@ EFI_STATUS pe_memory_locate_sections( n_section_table, section_names, PTR_TO_SIZE(base), + size_in_memory, sections); return EFI_SUCCESS; @@ -663,7 +744,8 @@ EFI_STATUS pe_memory_locate_sections( EFI_STATUS pe_section_table_from_file( EFI_FILE *handle, PeSectionHeader **ret_section_table, - size_t *ret_n_section_table) { + size_t *ret_n_section_table, + size_t *ret_size_in_memory) { EFI_STATUS err; size_t len; @@ -717,6 +799,8 @@ EFI_STATUS pe_section_table_from_file( *ret_section_table = TAKE_PTR(section_table); *ret_n_section_table = n_section_table; + if (ret_size_in_memory) + *ret_size_in_memory = pe.OptionalHeader.SizeOfImage; return EFI_SUCCESS; } @@ -784,6 +868,7 @@ EFI_STATUS pe_locate_profile_sections( const char* const section_names[], unsigned profile, size_t validate_base, + size_t size_in_memory, PeSectionVector sections[]) { assert(section_table || n_section_table == 0); @@ -805,6 +890,7 @@ EFI_STATUS pe_locate_profile_sections( n, section_names, validate_base, + size_in_memory, sections); return EFI_SUCCESS; diff --git a/src/boot/pe.h b/src/boot/pe.h index 5c8dc86fe9389..bc9963799f519 100644 --- a/src/boot/pe.h +++ b/src/boot/pe.h @@ -36,13 +36,16 @@ static inline bool PE_SECTION_VECTOR_IS_SET(const PeSectionVector *v) { EFI_STATUS pe_section_table_from_base( const void *base, + size_t base_len, const PeSectionHeader **ret_section_table, - size_t *ret_n_section_table); + size_t *ret_n_section_table, + size_t *ret_size_in_memory); EFI_STATUS pe_section_table_from_file( EFI_FILE *handle, PeSectionHeader **ret_section_table, - size_t *ret_n_section_table); + size_t *ret_n_section_table, + size_t *ret_size_in_memory); EFI_STATUS pe_locate_profile_sections( const PeSectionHeader section_table[], @@ -50,15 +53,18 @@ EFI_STATUS pe_locate_profile_sections( const char* const section_names[], unsigned profile, size_t validate_base, + size_t size_in_memory, PeSectionVector sections[]); EFI_STATUS pe_memory_locate_sections( const void *base, + size_t base_len, const char *const section_names[], PeSectionVector sections[]); EFI_STATUS pe_kernel_info( const void *base, + size_t base_len, uint32_t *ret_entry_point, uint32_t *ret_compat_entry_point, size_t *ret_size_in_memory, diff --git a/src/boot/stub.c b/src/boot/stub.c index e69faca00b0bf..94c4ac49e4bf6 100644 --- a/src/boot/stub.c +++ b/src/boot/stub.c @@ -596,7 +596,7 @@ static EFI_STATUS load_addons( if (err != EFI_SUCCESS) return log_error_status(err, "Failed to find protocol in %ls: %m", items[i]); - err = pe_memory_locate_sections(loaded_addon->ImageBase, unified_sections, sections); + err = pe_memory_locate_sections(loaded_addon->ImageBase, loaded_addon->ImageSize, unified_sections, sections); if (err != EFI_SUCCESS) { log_error_status(err, "Unable to locate embedded .cmdline/.dtb/.dtbauto/.efifw/.initrd/.ucode sections in %ls, ignoring: %m", @@ -1099,8 +1099,8 @@ static EFI_STATUS find_sections( assert(sections); const PeSectionHeader *section_table; - size_t n_section_table; - err = pe_section_table_from_base(loaded_image->ImageBase, §ion_table, &n_section_table); + size_t n_section_table, size_in_memory; + err = pe_section_table_from_base(loaded_image->ImageBase, loaded_image->ImageSize, §ion_table, &n_section_table, &size_in_memory); if (err != EFI_SUCCESS) return log_error_status(err, "Unable to locate PE section table: %m"); @@ -1111,6 +1111,7 @@ static EFI_STATUS find_sections( unified_sections, /* profile= */ UINT_MAX, /* validate_base= */ PTR_TO_SIZE(loaded_image->ImageBase), + size_in_memory, sections); if (err != EFI_SUCCESS) return log_error_status(err, "Unable to locate embedded base PE sections: %m"); @@ -1123,6 +1124,7 @@ static EFI_STATUS find_sections( unified_sections, profile, /* validate_base= */ PTR_TO_SIZE(loaded_image->ImageBase), + size_in_memory, sections); if (err != EFI_SUCCESS && !(err == EFI_NOT_FOUND && profile == 0)) /* the first profile is implied if it doesn't exist */ return log_error_status(err, "Unable to locate embedded per-profile PE sections: %m"); From ba0c1c617e7154bb18e3f0911e07489db2000d45 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 22:10:20 +0100 Subject: [PATCH 60/71] boot: restore RW/RO memory attributes on every error linux_exec() marks code sections RO+X for W^X and reverts them to RW+NX in a loop just before returning, because EDK2 requires freed buffers to be writable and non-executable or FreePages() crashes. Not every error path is currently covered. Switch to a _cleanup_ helper so that every return path is covered. Follow-up for 56d19b633d049035afe3f690fd6c717e06f88597 --- src/boot/linux.c | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/boot/linux.c b/src/boot/linux.c index 584fc051b71bf..56b932ecb563d 100644 --- a/src/boot/linux.c +++ b/src/boot/linux.c @@ -153,6 +153,25 @@ static EFI_STATUS memory_mark_rw_nx(EFI_MEMORY_ATTRIBUTE_PROTOCOL *memory_proto, return EFI_SUCCESS; } +typedef struct CleanupNxSections { + EFI_MEMORY_ATTRIBUTE_PROTOCOL *memory_proto; + struct iovec *sections; + size_t n_sections; +} CleanupNxSections; + +static void cleanup_nx_sections(CleanupNxSections *c) { + assert(c); + + /* Restore the code sections that were marked RO+X back to RW+NX before their backing pages are + * freed: EDK2 requires freed buffers to be writable and non-executable (it may overwrite them with + * a fixed pattern), otherwise FreePages() crashes. */ + if (c->memory_proto) + for (size_t i = 0; i < c->n_sections; i++) + (void) memory_mark_rw_nx(c->memory_proto, &c->sections[i]); + + free(c->sections); +} + EFI_STATUS linux_exec( EFI_HANDLE parent_image, const char16_t *cmdline, @@ -242,8 +261,6 @@ EFI_STATUS linux_exec( * https://microsoft.github.io/mu/WhatAndWhy/enhancedmemoryprotection/ * https://www.kraxel.org/blog/2023/12/uefi-nx-linux-boot/ */ EFI_MEMORY_ATTRIBUTE_PROTOCOL *memory_proto = NULL; - _cleanup_free_ struct iovec *nx_sections = NULL; - size_t n_nx_sections = 0; if (pe_kernel_check_nx_compat(kernel->iov_base)) { /* LocateProtocol() is not quite that quick if you have many protocols, so only look for it @@ -276,6 +293,12 @@ EFI_STATUS linux_exec( /* addr= */ 0); uint8_t* loaded_kernel = PHYSICAL_ADDRESS_TO_POINTER(loaded_kernel_pages.addr); + + /* Any code section marked RO+X must be reverted to RW+NX before the backing pages are freed. */ + _cleanup_(cleanup_nx_sections) CleanupNxSections nx_restore = { + .memory_proto = memory_proto, + }; + FOREACH_ARRAY(h, headers, n_headers) { if (h->PointerToRelocations != 0) return log_error_status(EFI_LOAD_ERROR, "Inner kernel image contains sections with relocations, which we do not support."); @@ -304,15 +327,14 @@ EFI_STATUS linux_exec( /* Not a code section? Nothing to do, leave as-is. */ if (memory_proto && (h->Characteristics & (PE_CODE|PE_EXECUTE))) { - nx_sections = xrealloc(nx_sections, n_nx_sections * sizeof(struct iovec), (n_nx_sections + 1) * sizeof(struct iovec)); - nx_sections[n_nx_sections].iov_base = loaded_kernel + h->VirtualAddress; - nx_sections[n_nx_sections].iov_len = h->VirtualSize; + /* Record the section for cleanup before marking it RO+X: if memory_mark_ro_x() + * fails after partially applying the attributes, cleanup still reverts them. */ + nx_restore.sections = xrealloc(nx_restore.sections, nx_restore.n_sections * sizeof(struct iovec), (nx_restore.n_sections + 1) * sizeof(struct iovec)); + nx_restore.sections[nx_restore.n_sections++] = IOVEC_MAKE(loaded_kernel + h->VirtualAddress, h->VirtualSize); - err = memory_mark_ro_x(memory_proto, &nx_sections[n_nx_sections]); + err = memory_mark_ro_x(memory_proto, &nx_restore.sections[nx_restore.n_sections - 1]); if (err != EFI_SUCCESS) return err; - - ++n_nx_sections; } } @@ -352,11 +374,5 @@ EFI_STATUS linux_exec( /* Restore */ *parent_loaded_image = original_parent_loaded_image; - /* On failure we'll free the buffers. EDK2 requires the memory buffers to be writable and - * non-executable, as in some configurations it will overwrite them with a fixed pattern, so if the - * attributes are not restored FreePages() will crash. */ - for (size_t i = 0; i < n_nx_sections; i++) - (void) memory_mark_rw_nx(memory_proto, &nx_sections[i]); - return log_error_status(err, "Error starting kernel image: %m"); } From baad1744bd0bd4302ee438c7f1ba60217e758199 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 22:17:28 +0100 Subject: [PATCH 61/71] boot: restore parent loaded image when initrd registration fails linux_exec() patches the stub's own EFI_LOADED_IMAGE_PROTOCOL to point at the loaded inner kernel, and restores the saved original only after the entry point returns. The initrd_register() failure path returns without restoring, leaving the firmware's protocol pointing to freed data. Follow-up for f4051650657cd337ceba67b773f0e3bf854cbaff --- src/boot/linux.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/boot/linux.c b/src/boot/linux.c index 56b932ecb563d..67ed859993126 100644 --- a/src/boot/linux.c +++ b/src/boot/linux.c @@ -355,8 +355,12 @@ EFI_STATUS linux_exec( _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL; err = initrd_register(initrd, &initrd_handle); - if (err != EFI_SUCCESS) + if (err != EFI_SUCCESS) { + /* Restore the patched fields before kernel_file_path and loaded_kernel_pages are freed, + * otherwise the stub's own EFI_LOADED_IMAGE_PROTOCOL is left pointing at freed memory. */ + *parent_loaded_image = original_parent_loaded_image; return log_error_status(err, "Error registering initrd: %m"); + } log_wait(); From 918e8f9dd91e5f51cd06b599ef3cbe534aaff28d Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 26 Jun 2026 23:12:54 +0100 Subject: [PATCH 62/71] boot: require a minimum PE optional header size in verify_pe() verify_pe() only checked SizeOfOptionalHeader against a SIZE_MAX wrap (a clause that, given SizeOfOptionalHeader is a uint16_t, can never reject anything) and never read NumberOfRvaAndSizes. But pe_kernel_info(), pe_kernel_check_nx_compat() and pe_kernel_check_no_relocation() then read SizeOfImage, AddressOfEntryPoint, DllCharacteristics and the base relocation data directory entry from the optional header. Require SizeOfOptionalHeader to be large enough to contain everything down to the base relocation data directory entry, and require the image to declare that many data directory entries. Follow-up for bacc2ed0d5bb10de5d37a1df73c061247f005b59 --- src/boot/pe.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/boot/pe.c b/src/boot/pe.c index 5e4c07e1610c8..b7974b33a5089 100644 --- a/src/boot/pe.c +++ b/src/boot/pe.c @@ -140,6 +140,9 @@ typedef struct PeFileHeader { #define SECTION_TABLE_BYTES_MAX (16U * 1024U * 1024U) +/* https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only */ +#define BASE_RELOCATION_TABLE_DATA_DIRECTORY_ENTRY 5 + static bool verify_dos(const DosFileHeader *dos) { assert(dos); @@ -163,7 +166,18 @@ static bool verify_pe( (allow_compatibility && pe->FileHeader.Machine == TARGET_MACHINE_TYPE_COMPATIBILITY)) && pe->FileHeader.NumberOfSections > 0 && IN_SET(pe->OptionalHeader.Magic, OPTHDR32_MAGIC, OPTHDR64_MAGIC) && - pe->FileHeader.SizeOfOptionalHeader < SIZE_MAX - (dos->ExeHeader + offsetof(PeFileHeader, OptionalHeader)); + pe->FileHeader.SizeOfOptionalHeader < SIZE_MAX - (dos->ExeHeader + offsetof(PeFileHeader, OptionalHeader)) && + /* The optional header must be large enough to actually contain every field we read from + * it later (the deepest being the base relocation data directory entry), and must declare + * at least that many data directory entries. */ + pe->FileHeader.SizeOfOptionalHeader >= + (pe->OptionalHeader.Magic == OPTHDR32_MAGIC ? + offsetof(PeOptionalHeader, DataDirectory32) : + offsetof(PeOptionalHeader, DataDirectory64)) + + (BASE_RELOCATION_TABLE_DATA_DIRECTORY_ENTRY + 1) * sizeof(PeImageDataDirectory) && + (pe->OptionalHeader.Magic == OPTHDR32_MAGIC ? + pe->OptionalHeader.NumberOfRvaAndSizes32 : + pe->OptionalHeader.NumberOfRvaAndSizes64) > BASE_RELOCATION_TABLE_DATA_DIRECTORY_ENTRY; } static size_t section_table_offset(const DosFileHeader *dos, const PeFileHeader *pe) { @@ -627,9 +641,6 @@ EFI_STATUS pe_kernel_info( return EFI_SUCCESS; } -/* https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only */ -#define BASE_RELOCATION_TABLE_DATA_DIRECTORY_ENTRY 5 - /* We do not expect PE inner kernels to have any relocations. However that might be wrong for some * architectures, or it might change in the future. If the case of relocation arise, we should transform this * function in a function applying the relocations. However for now, since it would not be exercised and From 61ddb06754a3d7c5ba8d80b5dea148f2af6a5277 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 3 Jul 2026 16:05:55 +0200 Subject: [PATCH 63/71] test: register SysInstall Varlink IDL in test-varlink-idl.c --- src/libsystemd/sd-varlink/test-varlink-idl.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libsystemd/sd-varlink/test-varlink-idl.c b/src/libsystemd/sd-varlink/test-varlink-idl.c index ae4a6d21b80f7..0619e6d8bf5f4 100644 --- a/src/libsystemd/sd-varlink/test-varlink-idl.c +++ b/src/libsystemd/sd-varlink/test-varlink-idl.c @@ -52,6 +52,7 @@ #include "varlink-io.systemd.Resolve.Monitor.h" #include "varlink-io.systemd.Shutdown.h" #include "varlink-io.systemd.StorageProvider.h" +#include "varlink-io.systemd.SysInstall.h" #include "varlink-io.systemd.SysUpdate.h" #include "varlink-io.systemd.SysUpdate.Notify.h" #include "varlink-io.systemd.Udev.h" @@ -231,6 +232,7 @@ TEST(parse_format) { &vl_interface_io_systemd_Resolve_Monitor, &vl_interface_io_systemd_Shutdown, &vl_interface_io_systemd_StorageProvider, + &vl_interface_io_systemd_SysInstall, &vl_interface_io_systemd_SysUpdate, &vl_interface_io_systemd_SysUpdate_Notify, &vl_interface_io_systemd_Udev, From 6a7f0e600cffbdfbdc21e778525d6dd43179df9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 3 Jul 2026 16:17:55 +0000 Subject: [PATCH 64/71] po: Translated using Weblate (Czech) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 99.3% (284 of 286 strings) Co-authored-by: Michal Čihař Translate-URL: https://translate.fedoraproject.org/projects/systemd/main/cs/ Translation: systemd/main --- po/cs.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/po/cs.po b/po/cs.po index 76c4e7ba623cb..64cc38081f8b5 100644 --- a/po/cs.po +++ b/po/cs.po @@ -6,12 +6,13 @@ # Pavel Borecki , 2023, 2024, 2025, 2026. # Jan Kalabza , 2025, 2026. # Honza Hejzl , 2026. +# Michal Čihař , 2026. msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 11:49+0200\n" -"PO-Revision-Date: 2026-06-07 14:00+0000\n" -"Last-Translator: Jan Kalabza \n" +"PO-Revision-Date: 2026-07-03 16:17+0000\n" +"Last-Translator: Michal Čihař \n" "Language-Team: Czech \n" "Language: cs\n" @@ -20,7 +21,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2);\n" -"X-Generator: Weblate 2026.6.1\n" +"X-Generator: Weblate 2026.7.1.dev0\n" #: src/core/org.freedesktop.systemd1.policy.in:22 msgid "Send passphrase back to system" @@ -1384,11 +1385,10 @@ msgid "Please verify user on security token to unlock." msgstr "" #: src/shared/libfido2-util.c:936 -#, fuzzy, c-format -#| msgid "Please enter security token PIN:" +#, c-format msgid "" "Please enter security token PIN (remaining attempts before lock-out: %d):" -msgstr "Zadejte PIN bezpečnostního tokenu:" +msgstr "Zadejte PIN bezpečnostního tokenu (zbývající pokusy před zamčením: %d):" #: src/shared/libfido2-util.c:948 msgid "Please enter security token PIN:" From 85b8fd5879569022e5d8e8fa230d4a171b9091ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9ane=20GRASSER?= Date: Fri, 3 Jul 2026 16:17:56 +0000 Subject: [PATCH 65/71] po: Translated using Weblate (French) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (286 of 286 strings) Co-authored-by: Léane GRASSER Translate-URL: https://translate.fedoraproject.org/projects/systemd/main/fr/ Translation: systemd/main --- po/fr.po | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/po/fr.po b/po/fr.po index 21cc88571d563..aea53d70f18d5 100644 --- a/po/fr.po +++ b/po/fr.po @@ -12,7 +12,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 11:49+0200\n" -"PO-Revision-Date: 2026-06-09 09:04+0000\n" +"PO-Revision-Date: 2026-07-03 16:17+0000\n" "Last-Translator: Léane GRASSER \n" "Language-Team: French \n" @@ -21,7 +21,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 2026.6.1\n" +"X-Generator: Weblate 2026.7.1.dev0\n" #: src/core/org.freedesktop.systemd1.policy.in:22 msgid "Send passphrase back to system" @@ -1489,25 +1489,26 @@ msgstr "" #: src/shared/libfido2-util.c:500 src/shared/libfido2-util.c:557 msgid "Please confirm presence on security token to unlock." msgstr "" -"Veuillez confirmer votre présence sur la clef de sécurité pour le " +"Confirmez votre présence sur la clé de sécurité pour procéder au " "déverrouillage." #: src/shared/libfido2-util.c:516 msgid "Please verify user on security token to unlock." msgstr "" -"Veuillez confirmer l'utilisateur sur la clef de sécurité pour le " +"Confirmez l'utilisateur sur la clé de sécurité pour procéder au " "déverrouillage." #: src/shared/libfido2-util.c:936 -#, fuzzy, c-format -#| msgid "Please enter security token PIN:" +#, c-format msgid "" "Please enter security token PIN (remaining attempts before lock-out: %d):" -msgstr "Veuillez entrer le code PIN de la clef de sécurité:" +msgstr "" +"Saisissez le PIN de la clé de sécurité (tentatives restantes avant blocage : " +"%d) :" #: src/shared/libfido2-util.c:948 msgid "Please enter security token PIN:" -msgstr "Veuillez entrer le code PIN de la clef de sécurité:" +msgstr "Saisissez le PIN de la clé de sécurité :" #~ msgid "Cleanup old system updates" #~ msgstr "Nettoyer les anciennes mises à jour système" From 40533dcb860bdb4b70f34067e9517bd14a40e13a Mon Sep 17 00:00:00 2001 From: Jesse Guo Date: Fri, 3 Jul 2026 16:17:56 +0000 Subject: [PATCH 66/71] po: Translated using Weblate (Chinese (Simplified) (zh_CN)) Currently translated at 100.0% (286 of 286 strings) Co-authored-by: Jesse Guo Translate-URL: https://translate.fedoraproject.org/projects/systemd/main/zh_CN/ Translation: systemd/main --- po/zh_CN.po | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/po/zh_CN.po b/po/zh_CN.po index 35ad8424ce74d..5d7e710a59460 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -18,8 +18,8 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 11:49+0200\n" -"PO-Revision-Date: 2026-06-12 09:22+0000\n" -"Last-Translator: Luke Na \n" +"PO-Revision-Date: 2026-07-03 16:17+0000\n" +"Last-Translator: Jesse Guo \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_CN\n" @@ -27,7 +27,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 2026.6.1\n" +"X-Generator: Weblate 2026.7.1.dev0\n" #: src/core/org.freedesktop.systemd1.policy.in:22 msgid "Send passphrase back to system" @@ -1302,11 +1302,10 @@ msgid "Please verify user on security token to unlock." msgstr "请在安全令牌上验证用户身份以解锁。" #: src/shared/libfido2-util.c:936 -#, fuzzy, c-format -#| msgid "Please enter security token PIN:" +#, c-format msgid "" "Please enter security token PIN (remaining attempts before lock-out: %d):" -msgstr "请输入安全令牌 PIN:" +msgstr "请输入安全令牌 PIN(锁定前剩余尝试次数:%d):" #: src/shared/libfido2-util.c:948 msgid "Please enter security token PIN:" From 85a210c688bc4730e74021293253e8842567f5fa Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 19 Jun 2026 18:35:49 +0200 Subject: [PATCH 67/71] varlink: properly linebreak upgrade/more flag generation Let's not manually create comments, but use the usual varlink_idl_format_comment() function which does line break handling properly. This polishes the output for very narrow outputs, since we'll line break these synthetic comments too. --- src/libsystemd/sd-varlink/sd-varlink-idl.c | 34 +++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/libsystemd/sd-varlink/sd-varlink-idl.c b/src/libsystemd/sd-varlink/sd-varlink-idl.c index 55f9b8b1d0f4e..95b6b8d039f4d 100644 --- a/src/libsystemd/sd-varlink/sd-varlink-idl.c +++ b/src/libsystemd/sd-varlink/sd-varlink-idl.c @@ -392,23 +392,29 @@ static int varlink_idl_format_symbol( * Until this is resolved upstream, consider this comment part of the API (i.e. don't change * only extend). It is used by tools like varlink-http-bridge. */ if ((symbol->symbol_flags & (SD_VARLINK_REQUIRES_MORE|SD_VARLINK_SUPPORTS_MORE)) != 0) { - fputs(colors[COLOR_COMMENT], f); - if (FLAGS_SET(symbol->symbol_flags, SD_VARLINK_REQUIRES_MORE)) - fputs("# [Requires 'more' flag]", f); - else - fputs("# [Supports 'more' flag]", f); - fputs(colors[COLOR_RESET], f); - fputs("\n", f); + const char *msg = FLAGS_SET(symbol->symbol_flags, SD_VARLINK_REQUIRES_MORE) ? "[Requires 'more' flag]" : "[Supports 'more' flag]"; + + r = varlink_idl_format_comment( + f, + msg, + /* indent= */ NULL, + colors, + cols); + if (r < 0) + return r; } if ((symbol->symbol_flags & (SD_VARLINK_REQUIRES_UPGRADE|SD_VARLINK_SUPPORTS_UPGRADE)) != 0) { - fputs(colors[COLOR_COMMENT], f); - if (FLAGS_SET(symbol->symbol_flags, SD_VARLINK_REQUIRES_UPGRADE)) - fputs("# [Requires 'upgrade' flag]", f); - else - fputs("# [Supports 'upgrade' flag]", f); - fputs(colors[COLOR_RESET], f); - fputs("\n", f); + const char *msg = FLAGS_SET(symbol->symbol_flags, SD_VARLINK_REQUIRES_UPGRADE) ? "[Requires 'upgrade' flag]" : "[Supports 'upgrade' flag]"; + + r = varlink_idl_format_comment( + f, + msg, + /* indent= */ NULL, + colors, + cols); + if (r < 0) + return r; } fputs(colors[COLOR_SYMBOL_TYPE], f); From 7961b4a78df9723e0de1fcac762f4d6e2d7b622b Mon Sep 17 00:00:00 2001 From: "Matthias P. Walther" Date: Fri, 3 Jul 2026 23:21:46 +0200 Subject: [PATCH 68/71] hwdb: map mic-mute key on Logitech K950 (Bluetooth) The mic-mute key on the Logitech K950 keyboard (046D:B388, Bluetooth) emits BTN_0 (scancode 0x100e1) instead of a usable key, so it does nothing under GNOME/KDE. Map it to KEY_MICMUTE, its intended function. Scancode determined with evtest on the device. --- hwdb.d/60-keyboard.hwdb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hwdb.d/60-keyboard.hwdb b/hwdb.d/60-keyboard.hwdb index 0494f0311f0da..097714d8b897b 100644 --- a/hwdb.d/60-keyboard.hwdb +++ b/hwdb.d/60-keyboard.hwdb @@ -1432,6 +1432,10 @@ evdev:input:b0005v046DpB317* KEYBOARD_KEY_70047=brightnessdown KEYBOARD_KEY_70048=brightnessup +# Logitech K950 +evdev:input:b0005v046DpB388* + KEYBOARD_KEY_100e1=micmute # Mic-mute key emits BTN_0 + # iTouch evdev:input:b0003v046DpC308* KEYBOARD_KEY_90001=shop # Shopping From 67f72f17653753b1f067a6ed5027b2334b5e7602 Mon Sep 17 00:00:00 2001 From: Nicholas Lim Date: Sat, 4 Jul 2026 18:06:01 +0800 Subject: [PATCH 69/71] hwdb: Make Amlogic burn mode work out-of-box --- hwdb.d/70-debug-appliance.hwdb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hwdb.d/70-debug-appliance.hwdb b/hwdb.d/70-debug-appliance.hwdb index 8f535f750bb48..701df4b77d093 100644 --- a/hwdb.d/70-debug-appliance.hwdb +++ b/hwdb.d/70-debug-appliance.hwdb @@ -5,6 +5,16 @@ # Permitted keys: # ID_DEBUG_APPLIANCE=?* +# Amlogic devices in burn mode +# Used to interact with devices over Worldcup / ADNL protocol, see: +# https://wiki.coreelec.org/coreelec:aml_usb_tool +# +# The idVendor and idProduct used are documented in source code here: +# https://github.com/superna9999/pyamlboot/blob/d2857f4371bcdb7a90d885f24987e03b6d647a8a/PROTOCOL.md?plain=1#L17 +usb:v1B8EpC003* +usb:v1B8EpC004* + ID_DEBUG_APPLIANCE=aml_burn_mode + # Samsung devices in download mode # Used to interact with devices over Odin protocol, see: # https://en.wikipedia.org/wiki/Odin_(firmware_flashing_software) From ce20e29da71130c425512e292fb6af1c4ddd95cb Mon Sep 17 00:00:00 2001 From: Simran Singh Date: Wed, 10 Dec 2025 06:14:53 +0530 Subject: [PATCH 70/71] clonesetup: add support to clone devices via /etc/clonetab Adds dm-clone device setup at boot via a new /etc/clonetab config file, following the crypttab/veritytab pattern. - Add systemd-clonesetup-generator to parse /etc/clonetab and generate units. - Add systemd-clonesetup binary to create/remove dm-clone devices via ioctl. - Add clonesetup.target for ordering dm-clone activation at boot. - Add region_size= option in clonetab to configure dm-clone hydration granularity. - Add clonetab(5) and systemd-clonesetup-generator(8) man pages. Fixes: https://github.com/systemd/systemd/issues/39500 --- docs/ENVIRONMENT.md | 4 + man/clonetab.xml | 121 ++++++ man/rules/meson.build | 6 + man/systemd-clonesetup-generator.xml | 50 +++ man/systemd-clonesetup.xml | 75 ++++ meson.build | 2 + src/clonesetup/clonesetup-generator.c | 265 ++++++++++++++ src/clonesetup/clonesetup-ioctl.c | 262 +++++++++++++ src/clonesetup/clonesetup-ioctl.h | 16 + src/clonesetup/clonesetup.c | 245 +++++++++++++ src/clonesetup/meson.build | 17 + .../TEST-93-CLONESETUP/meson.build | 8 + test/integration-tests/meson.build | 1 + test/units/TEST-93-CLONESETUP.sh | 344 ++++++++++++++++++ units/clonesetup.target | 12 + units/meson.build | 4 + 16 files changed, 1432 insertions(+) create mode 100644 man/clonetab.xml create mode 100644 man/systemd-clonesetup-generator.xml create mode 100644 man/systemd-clonesetup.xml create mode 100644 src/clonesetup/clonesetup-generator.c create mode 100644 src/clonesetup/clonesetup-ioctl.c create mode 100644 src/clonesetup/clonesetup-ioctl.h create mode 100644 src/clonesetup/clonesetup.c create mode 100644 src/clonesetup/meson.build create mode 100644 test/integration-tests/TEST-93-CLONESETUP/meson.build create mode 100755 test/units/TEST-93-CLONESETUP.sh create mode 100644 units/clonesetup.target diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 1d2498e206f6d..71df55896a550 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -76,6 +76,10 @@ All tools: `/etc/veritytab`. Only useful for debugging. Currently only supported by `systemd-veritysetup-generator`. +* `$SYSTEMD_CLONETAB` — if set, use this path instead of + `/etc/clonetab`. Only useful for debugging. Currently only supported by + `systemd-clonesetup-generator`. + * `$SYSTEMD_DEFAULT_HOSTNAME` — override the compiled-in fallback hostname (relevant in particular for the system manager and `systemd-hostnamed`). Must be a valid hostname (either a single label or a FQDN). diff --git a/man/clonetab.xml b/man/clonetab.xml new file mode 100644 index 0000000000000..651fa339cd163 --- /dev/null +++ b/man/clonetab.xml @@ -0,0 +1,121 @@ + + + + + + + + clonetab + systemd + + + + clonetab + 5 + + + + clonetab + Configuration for dm-clone block devices + + + + /etc/clonetab + + + + Description + + The /etc/clonetab file describes + dm-clone block devices that are set up during system boot. + + Empty lines and lines starting with the # + character are ignored. Each of the remaining lines describes one + dm-clone device. Fields are delimited by white space. + + Each line is in the formname source-device destination-device metadata-device [options] + The first four fields are mandatory, the fifth is optional. + + The five fields of /etc/clonetab are defined as follows: + + + + The first field contains the name of the resulting dm-clone device; its + block device is set up below /dev/mapper/. + + The second field contains a path to the read-only source block device. + This is the device whose data is cloned to the destination device. Reads to regions not + yet hydrated are served directly from this device. + + The third field contains a path to the writable destination block device. + The source device's data is copied here in the background. It must be at least as + large as the source device. + + The fourth field contains a path to the metadata block device. This + small device tracks which regions of the destination have been hydrated and is managed + exclusively by dm-clone. + + The fifth field, if present, contains comma-separated key=value + options. The following option is supported: + + + + + Controls the granularity of background hydration copying — how much + data is copied at a time. Region size is specified in 512-byte sectors, so + region_size=8 means 8 × 512 = 4KB per region. One region is the + atomic unit dm-clone tracks: it is either fully hydrated (copied to the destination) + or not, never partially. If a copy is interrupted mid-region, that whole region is + retried from scratch on next boot. Smaller regions mean finer progress tracking; + larger regions reduce metadata overhead. Must be a power of two between + 8 and 2097152 sectors. Defaults to + 8 (4KB). For background, see + dm-clone kernel documentation. + Device mapper sectors are always 512 bytes regardless of physical block size + (include/linux/blk_types.h: SECTOR_SIZE = 1 << 9). + + + + + + + If no options are needed, the field may be omitted entirely or + - may be used as a placeholder. + + + + + At early boot and when the system manager configuration is reloaded, this file is + translated into native systemd units by + systemd-clonesetup-generator8. + + + + Examples + + + Simple clone without options + Clone a source device to a destination, using a separate metadata device: + mydevice /dev/sdb /dev/sdc /dev/sdd + + + + Clone with custom region size + Clone a source device to a destination with a custom region size of 16 sectors: + mydevice /dev/sdb /dev/sdc /dev/sdd region_size=16 + + + + + See Also + + systemd1 + systemd-clonesetup-generator8 + dmsetup8 + + + + diff --git a/man/rules/meson.build b/man/rules/meson.build index 13025b8993cf7..bac836015b8fb 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -15,6 +15,7 @@ manpages = [ ['bootup', '7', [], ''], ['busctl', '1', [], ''], ['capsule@.service', '5', [], ''], + ['clonetab', '5', [], ''], ['coredump.conf', '5', ['coredump.conf.d'], 'ENABLE_COREDUMP'], ['coredumpctl', '1', [], 'ENABLE_COREDUMP'], ['crypttab', '5', [], 'HAVE_LIBCRYPTSETUP'], @@ -1024,6 +1025,11 @@ manpages = [ ['systemd-cat', '1', [], ''], ['systemd-cgls', '1', [], ''], ['systemd-cgtop', '1', [], ''], + ['systemd-clonesetup', + '8', + ['systemd-clonesetup@.service'], + ''], + ['systemd-clonesetup-generator', '8', [], ''], ['systemd-coredump', '8', ['systemd-coredump.socket', 'systemd-coredump@.service'], diff --git a/man/systemd-clonesetup-generator.xml b/man/systemd-clonesetup-generator.xml new file mode 100644 index 0000000000000..28c8b5ad004c4 --- /dev/null +++ b/man/systemd-clonesetup-generator.xml @@ -0,0 +1,50 @@ + + + + + + + + systemd-clonesetup-generator + systemd + + + + systemd-clonesetup-generator + 8 + + + + systemd-clonesetup-generator + Unit generator for /etc/clonetab + + + + /usr/lib/systemd/system-generators/systemd-clonesetup-generator + + + + Description + + systemd-clonesetup-generator is a generator that translates + /etc/clonetab into native systemd units early at boot and when + configuration of the system manager is reloaded. This will create + systemd-clonesetup@.service8 + units as necessary. + + systemd-clonesetup-generator implements + systemd.generator7. + + + + See Also + + systemd1 + clonetab5 + systemd-clonesetup@.service8 + dmsetup8 + + + + diff --git a/man/systemd-clonesetup.xml b/man/systemd-clonesetup.xml new file mode 100644 index 0000000000000..f157b8cc28ffb --- /dev/null +++ b/man/systemd-clonesetup.xml @@ -0,0 +1,75 @@ + + + + + + + + systemd-clonesetup + systemd + + + + systemd-clonesetup + 8 + + + + systemd-clonesetup + systemd-clonesetup@.service + Set up and tear down dm-clone devices + + + + + systemd-clonesetup + add + NAME + SOURCE-DEVICE + DEST-DEVICE + META-DEVICE + OPTIONS + + + + systemd-clonesetup + remove + NAME + + + systemd-clonesetup@.service + + + + Description + + systemd-clonesetup is used to set up (with add) and tear down + (with remove) dm-clone devices. It is primarily used via + systemd-clonesetup@.service units generated from + /etc/clonetab by + systemd-clonesetup-generator8. + + + The positional arguments NAME, SOURCE-DEVICE, + DEST-DEVICE, META-DEVICE, and OPTIONS + have the same meaning as the fields in + clonetab5. + + Currently, the supported option in OPTIONS is + region_size=N, where + N is measured in 512-byte sectors. + + + + See Also + + systemd1 + clonetab5 + systemd-clonesetup-generator8 + systemd.generator7 + dmsetup8 + + + + diff --git a/meson.build b/meson.build index 1ce4c95368da3..fc5a1b72bb6fa 100644 --- a/meson.build +++ b/meson.build @@ -302,6 +302,7 @@ conf.set_quoted('SYSTEMD_USERWORK_PATH', libexecdir / 'syst conf.set_quoted('SYSTEMD_MOUNTWORK_PATH', libexecdir / 'systemd-mountwork') conf.set_quoted('SYSTEMD_NSRESOURCEWORK_PATH', libexecdir / 'systemd-nsresourcework') conf.set_quoted('SYSTEMD_VERITYSETUP_PATH', libexecdir / 'systemd-veritysetup') +conf.set_quoted('SYSTEMD_CLONESETUP_PATH', libexecdir / 'systemd-clonesetup') conf.set_quoted('SYSTEM_CONFIG_UNIT_DIR', pkgsysconfdir / 'system') conf.set_quoted('SYSTEM_DATA_UNIT_DIR', systemunitdir) conf.set_quoted('SYSTEM_ENV_GENERATOR_DIR', systemenvgeneratordir) @@ -2129,6 +2130,7 @@ subdir('src/bootctl') subdir('src/busctl') subdir('src/cgls') subdir('src/cgtop') +subdir('src/clonesetup') subdir('src/coredump') subdir('src/creds') subdir('src/cryptenroll') diff --git a/src/clonesetup/clonesetup-generator.c b/src/clonesetup/clonesetup-generator.c new file mode 100644 index 0000000000000..00ac696c4ac37 --- /dev/null +++ b/src/clonesetup/clonesetup-generator.c @@ -0,0 +1,265 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "alloc-util.h" +#include "dropin.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "generator.h" +#include "log.h" +#include "path-util.h" +#include "specifier.h" +#include "string-util.h" +#include "unit-name.h" + +static const char *arg_dest = NULL; + +/* Generate unit files that call the systemd-clonesetup binary to create or remove clone devices. */ +static int generate_clone_units( + const char *clone_name, + const char *source_dev, + const char *dest_dev, + const char *metadata_dev, + const char *options) { + + /* unit files for each device */ + _cleanup_fclose_ FILE *f = NULL; + int r; + + assert(clone_name); + assert(source_dev); + assert(dest_dev); + assert(metadata_dev); + + /* Escape clone name for unit specifiers and then ExecStart/ExecStop parsing. */ + _cleanup_free_ char *clone_name_spec_escaped = NULL; + clone_name_spec_escaped = specifier_escape(clone_name); + if (!clone_name_spec_escaped) + return log_oom(); + + /* create clone_dev_path that holds path for new cloned device */ + _cleanup_free_ char *clone_dev_path = NULL, *clone_dev_path_escaped = NULL, + *clone_dev_path_unit_escaped = NULL; + clone_dev_path = path_join("/dev/mapper", clone_name); + if (!clone_dev_path) + return log_oom(); + + clone_dev_path_escaped = specifier_escape(clone_dev_path); + if (!clone_dev_path_escaped) + return log_oom(); + + r = unit_name_path_escape(clone_dev_path, &clone_dev_path_unit_escaped); + if (r < 0) + return log_error_errno(r, "Failed to escape clone device path: %m"); + + _cleanup_free_ char *e = NULL, *unit = NULL; + /* escape clone name */ + e = unit_name_escape(clone_name); + if (!e) + return log_oom(); + + /* Generate unit name for the clone service */ + r = unit_name_build("systemd-clonesetup", e, ".service", &unit); + if (r < 0) + return log_error_errno(r, "Failed to generate unit name: %m"); + + /* Generate unit names for dependencies */ + _cleanup_free_ char *source_unit = NULL, *dest_unit = NULL, *metadata_unit = NULL; + r = unit_name_from_path(source_dev, ".device", &source_unit); + if (r < 0) + return log_error_errno(r, "Failed to generate source device unit name: %m"); + + r = unit_name_from_path(dest_dev, ".device", &dest_unit); + if (r < 0) + return log_error_errno(r, "Failed to generate dest device unit name: %m"); + + r = unit_name_from_path(metadata_dev, ".device", &metadata_unit); + if (r < 0) + return log_error_errno(r, "Failed to generate metadata device unit name: %m"); + + /* Escape device paths for unit specifiers and then ExecStart parsing. */ + _cleanup_free_ char *source_spec_escaped = NULL, + *dest_spec_escaped = NULL, *metadata_spec_escaped = NULL, + *options_spec_escaped = NULL; + source_spec_escaped = specifier_escape(source_dev); + if (!source_spec_escaped) + return log_oom(); + + dest_spec_escaped = specifier_escape(dest_dev); + if (!dest_spec_escaped) + return log_oom(); + + metadata_spec_escaped = specifier_escape(metadata_dev); + if (!metadata_spec_escaped) + return log_oom(); + + if (options) { + options_spec_escaped = specifier_escape(options); + if (!options_spec_escaped) + return log_oom(); + } + + r = generator_open_unit_file(arg_dest, /* source = */ NULL, unit, &f); + if (r < 0) + return r; + + /* With DefaultDependencies=no, order after udev so backing /dev nodes are ready in early boot. + * The : exec prefix on ExecStart=/ExecStop= disables $ { } env-var expansion by the manager. */ + fprintf(f, + "[Unit]\n" + "Description=Create dm-clone device %1$s\n" + "Documentation=man:clonetab(5) man:systemd-clonesetup(8) man:systemd-clonesetup-generator(8)\n" + "DefaultDependencies=no\n" + "BindsTo=%2$s %3$s %4$s\n" + "After=%2$s %3$s %4$s systemd-udevd-kernel.socket\n" + "Before=blockdev@%5$s.target clonesetup.target shutdown.target\n" + "Wants=blockdev@%5$s.target\n" + "Conflicts=shutdown.target\n" + "\n" + "[Service]\n" + "Type=oneshot\n" + "RemainAfterExit=yes\n" + "ExecStart=:" SYSTEMD_CLONESETUP_PATH " add '%6$s' '%7$s' '%8$s' '%9$s' '%10$s'\n" + "ExecStop=:" SYSTEMD_CLONESETUP_PATH " remove '%6$s'\n" + "TimeoutSec=0\n", + clone_dev_path_escaped, + source_unit, dest_unit, metadata_unit, + clone_dev_path_unit_escaped, + clone_name_spec_escaped, source_spec_escaped, dest_spec_escaped, + metadata_spec_escaped, options_spec_escaped ?: "-"); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write unit %s: %m", unit); + + /* symlink unit file to enable it */ + _cleanup_free_ char *dmname = NULL; + r = unit_name_from_path(clone_dev_path, ".device", &dmname); + if (r < 0) + return log_error_errno(r, "Failed to generate clone device unit name: %m"); + + r = generator_add_symlink(arg_dest, dmname, "requires", unit); + if (r < 0) + return r; + + /* Extend device timeout to allow clone service to complete */ + r = write_drop_in(arg_dest, dmname, 40, "device-timeout", + "# Automatically generated by systemd-clonesetup-generator\n\n" + "[Unit]\n" + "JobRunningTimeoutSec=infinity\n"); + if (r < 0) + log_warning_errno(r, "Failed to write device timeout drop-in: %m"); + + /* Add to clonesetup.target so it starts at boot */ + r = generator_add_symlink(arg_dest, "clonesetup.target", "requires", unit); + if (r < 0) + return r; + + return 0; +} + +static int validate_dev_path(const char *what, const char *path) { + if (!string_is_safe(path, 0) || !path_is_valid(path) || !path_is_normalized(path) || + !path_is_absolute(path) || !path_startswith(path, "/dev/")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid %s device path '%s'.", what, path); + return 0; +} + +/* Field validation — what each check covers: + * control chars, \, ', whitespace → string_is_safe() on all fields + * / in name → string_is_safe(name, STRING_FILENAME) + * .. in device paths → path_is_normalized() + * non-/dev/ device paths → path_is_absolute() + path_startswith(path, "/dev/") + * % specifier expansion → specifier_escape() applied before writing unit file + * $ { } env-var expansion → : exec prefix on ExecStart=/ExecStop= */ +static int validate_fields(const char *fname, unsigned clone_line, const char *name, + const char *src, const char *dst, const char *meta, const char *options) { + int r; + if (!string_is_safe(name, STRING_FILENAME)) { + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid clone name '%s' in %s:%u, ignoring.", name, fname, clone_line); + } + + r = validate_dev_path("source", src); + if (r < 0) + return r; + r = validate_dev_path("destination", dst); + if (r < 0) + return r; + r = validate_dev_path("metadata", meta); + if (r < 0) + return r; + + if (options && !string_is_safe(options, 0)) { + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid options '%s' in %s:%u, ignoring.", options, fname, clone_line); + } + return 0; +} + +static int add_clone_devices(void) { + _cleanup_fclose_ FILE *f = NULL; + unsigned clone_line = 0; + int r, ret = 0; + const char *fname; + + fname = secure_getenv("SYSTEMD_CLONETAB") ?: "/etc/clonetab"; + + r = fopen_unlocked(fname, "re", &f); + if (r < 0) { + if (r != -ENOENT) + log_error_errno(r, "Failed to open %s: %m", fname); + return 0; + } + + for (;;) { + _cleanup_free_ char *line = NULL, *src = NULL, + *name = NULL, *dst = NULL, *meta = NULL, *options = NULL; + int k; + + r = read_stripped_line(f, LONG_LINE_MAX, &line); + if (r < 0) + return log_error_errno(r, "Failed to read %s: %m", fname); + if (r == 0) + break; + + clone_line++; + + if (IN_SET(line[0], 0, '#')) + continue; + + k = sscanf(line, "%ms %ms %ms %ms %ms", &name, &src, &dst, &meta, &options); + if (k < 4 || k > 5) { + log_error("Failed to parse %s:%u, ignoring.", fname, clone_line); + continue; + } + + r = validate_fields(fname, clone_line, name, src, dst, meta, options); + if (r < 0) + continue; + RET_GATHER(ret, generate_clone_units(name, src, dst, meta, options)); + } + + return ret; +} + +/* This generator reads /etc/clonetab and for each entry, writes unit files + * (creates systemd-clonesetup@.service and clonesetup.target.requires/systemd-clonesetup@.service) + * that clonesetup.target requires, and that run systemd-clonesetup (add device at boot, + * remove it at shutdown); systemd-clonesetup (used in systemd-clonesetup@.service) is the binary that + * uses device-mapper ioctls to create and remove the dm-clone devices. + * clonesetup.target groups these units so they run together at boot. + * Boot chain: sysinit.target has clonesetup.target in sysinit.target.wants/ (see units/meson.build), + * so at boot clonesetup.target starts and pulls in these units via clonesetup.target.requires/. */ +static int run(const char *dest, const char *dest_early, const char *dest_late) { + + /* dest usually is /run/systemd/generator */ + assert_se(arg_dest = dest); + + return add_clone_devices(); +} + +DEFINE_MAIN_GENERATOR_FUNCTION(run); diff --git a/src/clonesetup/clonesetup-ioctl.c b/src/clonesetup/clonesetup-ioctl.c new file mode 100644 index 0000000000000..812ce9db9cee9 --- /dev/null +++ b/src/clonesetup/clonesetup-ioctl.c @@ -0,0 +1,262 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include + +#include "sd-device.h" + +#include "clonesetup-ioctl.h" +#include "device-private.h" +#include "errno-util.h" +#include "fd-util.h" +#include "log.h" +#include "memory-util.h" +#include "string-util.h" + +/* Returns the size in bytes of the block device at dev_path. + * Loading the dm-clone table needs the source device size in sectors; sysfs + * reports size in 512-byte sectors. This reads sysfs and returns bytes so the + * caller can divide by 512 and pass the sector count to dm_clone_load_table(). */ +static int get_size(const char *dev_path, uint64_t *ret_size) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + uint64_t size; + int r; + + assert(dev_path); + assert(ret_size); + + r = sd_device_new_from_devname(&dev, dev_path); + if (r < 0) + return log_error_errno(r, "Failed to create device from '%s': %m", dev_path); + + r = device_get_sysattr_u64(dev, "size", &size); + if (r < 0) + return log_error_errno(r, "Failed to get device size for '%s': %m", dev_path); + + /* sysfs 'size' is in 512-byte sectors */ + *ret_size = u64_multiply_safe(size, 512); + if (*ret_size == 0 && size != 0) + return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), + "Device size overflow for '%s'", dev_path); + return 0; +} + +/* Common helper used to run dm ioctls. */ +static int dm_ioctl_run(const char *name, uint32_t cmd, struct dm_ioctl *data, size_t data_size) { + _cleanup_close_ int fd = -EBADF; + struct dm_ioctl *dm = data; + int r; + + assert(name); + assert(data); + assert(data_size >= sizeof(struct dm_ioctl)); + + dm->version[0] = DM_VERSION_MAJOR; + dm->version[1] = DM_VERSION_MINOR; + dm->version[2] = DM_VERSION_PATCHLEVEL; + dm->data_size = data_size; + + if (strlen(name) >= sizeof_field(struct dm_ioctl, name)) + return log_error_errno(SYNTHETIC_ERRNO(ENAMETOOLONG), + "DM device name too long: %s", name); + strncpy_exact(dm->name, name, sizeof(dm->name)); + + fd = open("/dev/mapper/control", O_RDWR | O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open /dev/mapper/control: %m"); + + r = RET_NERRNO(ioctl(fd, cmd, dm)); + if (r < 0) { + if (r == -ENXIO && cmd == DM_DEV_REMOVE) + return log_debug_errno(r, "DM ioctl failed: %m"); + return log_error_errno(r, "DM ioctl failed: %m"); + } + return 0; +} + +/* First dm ioctl needed to create a device. */ +static int dm_clone_create(const char *name) { + assert(name); + + struct dm_ioctl dm = {}; + return dm_ioctl_run(name, DM_DEV_CREATE, &dm, sizeof(dm)); +} + +/* Second dm ioctl needed to create a device. */ +static int dm_clone_load_table(const char *name, uint64_t size_sectors, const char *target_params) { + char *params_buf; + size_t params_len, dm_size; + _cleanup_free_ struct dm_ioctl *dm = NULL; + struct dm_target_spec *tgt; + + assert(name); + assert(target_params); + + params_len = strlen(target_params) + 1; + /* ensure that dm_size is always aligned, so it makes the buffer actually match what .next claims */ + dm_size = ALIGN8(sizeof(struct dm_ioctl)) + ALIGN8(sizeof(struct dm_target_spec) + params_len); + dm = malloc0(dm_size); + if (!dm) + return log_oom(); + *dm = (struct dm_ioctl) { + .data_start = ALIGN8(sizeof(struct dm_ioctl)), + .target_count = 1, + }; + + tgt = CAST_ALIGN_PTR(struct dm_target_spec, (uint8_t *) dm + dm->data_start); + *tgt = (struct dm_target_spec) { + .length = size_sectors, + /* Per linux/dm-ioctl.h: next is the byte offset from this dm_target_spec to the next one, + * rounded up to 8-byte alignment. With target_count == 1 next == 0 works, but set it + * correctly to avoid silent breakage if a second target is ever added. */ + .next = ALIGN8(sizeof(struct dm_target_spec) + params_len), + }; + strncpy(tgt->target_type, "clone", sizeof(tgt->target_type)); + + params_buf = (char *) ((uint8_t *) tgt + ALIGN8(sizeof(struct dm_target_spec))); + memcpy(params_buf, target_params, params_len); + + return dm_ioctl_run(name, DM_TABLE_LOAD, dm, dm_size); +} + +/* Third and final dm ioctl needed to create a device. */ +static int dm_clone_activate(const char *name) { + assert(name); + + struct dm_ioctl dm = {}; + + return dm_ioctl_run(name, DM_DEV_SUSPEND, &dm, sizeof(dm)); +} + +/* Calls multiple dm ioctls to create device. */ +int dm_clone_create_device( + const char *name, + const char *source_dev, + const char *dest_dev, + const char *metadata_dev, + uint64_t region_size) { + + _cleanup_free_ char *target_params = NULL; + uint64_t src_dev_size_sectors, src_dev_size; + int r; + + assert(name); + assert(source_dev); + assert(dest_dev); + assert(metadata_dev); + + r = get_size(source_dev, &src_dev_size); + if (r < 0) + return r; + + /* The device mapper kernel API always uses 512-byte sectors, regardless of the + * physical block size of the device (all DM targets use sector_t which is 512B). + * + * get_size internally uses sysfs i.e. /sys/block//size which also reports device size in + * 512-byte sectors. Before returning, get_size multiplies the size returned by sysfs to bytes. So we + * divide the received byte size by 512 to get the sector count for the DM table. */ + assert(src_dev_size % 512 == 0); + src_dev_size_sectors = src_dev_size / 512; + + /* dm-clone target params: [options] + * region_size = region size in sectors, configurable via clonetab (default 8 = 4KB regions with + * 512-byte sectors) 1 = hydration threshold (regions to hydrate per batch) no_hydration = don't + * start automatic background hydration + * + * The DM table "target_params" string is passed directly to the kernel via ioctl(fd, DM_TABLE_LOAD, + * ...) in dm_clone_load_table as a raw byte buffer. The kernel's DM table parser + * (drivers/md/dm-table.c) simply splits the params string on whitespace, so the only constraint is + * that the paths in params - metadata_dev, dest_dev, source_dev, and region_size must not contain + * spaces, which standard /dev/ paths never do, so the below args do NOT require shell escaping */ + if (asprintf(&target_params, "%s %s %s %" PRIu64 " 1 no_hydration", + metadata_dev, dest_dev, source_dev, region_size) < 0) + return log_oom(); + + r = dm_clone_create(name); + if (r < 0) + return r; + + r = dm_clone_load_table(name, src_dev_size_sectors, target_params); + if (r < 0) + goto fail; + + r = dm_clone_activate(name); + if (r < 0) + goto fail; + + log_info("Device %s active.", name); + return 0; + +fail: + (void) dm_clone_remove_device_deferred(name); + return r; +} + +/* Calls dm ioctl to send a message to the device. dm_ioctl is the kernel's generic device mapper envelope — + * every ioctl needs it. dm_target_msg is specific to the "send a message" operation + * */ +int dm_clone_send_message(const char *name, const char *message) { + _cleanup_free_ struct dm_ioctl *dm = NULL; + struct dm_target_msg *msg; + size_t dm_size, msg_len; + + assert(name); + assert(message); + + msg_len = strlen(message) + 1; + /* need to take into account both headers in size calculation */ + dm_size = ALIGN8(sizeof(struct dm_ioctl)) + sizeof(struct dm_target_msg) + msg_len; + dm = malloc0(dm_size); + if (!dm) + return log_oom(); + *dm = (struct dm_ioctl) { + /* with ALIGN8 call below, dm_target_msg starts at ALIGN8(sizeof(struct dm_ioctl)) which is + * already aligned, so the dm_target_msg struct lands correctly */ + .data_start = ALIGN8(sizeof(struct dm_ioctl)), + }; + + msg = CAST_ALIGN_PTR(struct dm_target_msg, (uint8_t *) dm + dm->data_start); + memcpy(msg->message, message, msg_len); + + return dm_ioctl_run(name, DM_TARGET_MSG, dm, dm_size); +} + +/* Calls dm ioctl to remove a device. flags if set can be used + * for deferred remove - e.g. DM_DEFERRED_REMOVE */ +static int dm_clone_remove_device_full(const char *name, uint32_t flags) { + struct dm_ioctl dm = { + .flags = flags, + }; + + assert(name); + + return dm_ioctl_run(name, DM_DEV_REMOVE, &dm, sizeof(dm)); +} + +/* Calls dm ioctl to remove a device. */ +int dm_clone_remove_device(const char *name) { + int r; + + assert(name); + r = dm_clone_remove_device_full(name, 0); + if (r < 0) + return r; + + log_info("Device %s inactive.", name); + return 0; +} + +/* Calls dm ioctl for deferred removal i.e. DM_DEFERRED_REMOVE */ +int dm_clone_remove_device_deferred(const char *name) { + int r; + + assert(name); + r = dm_clone_remove_device_full(name, DM_DEFERRED_REMOVE); + if (r < 0) + return r; + + log_info("Device %s marked for deferred removal.", name); + return 0; +} diff --git a/src/clonesetup/clonesetup-ioctl.h b/src/clonesetup/clonesetup-ioctl.h new file mode 100644 index 0000000000000..ba3762eb3ef1b --- /dev/null +++ b/src/clonesetup/clonesetup-ioctl.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +int dm_clone_create_device( + const char *name, + const char *source_dev, + const char *dest_dev, + const char *metadata_dev, + uint64_t region_size); + +int dm_clone_send_message(const char *name, const char *message); + +int dm_clone_remove_device(const char *name); +int dm_clone_remove_device_deferred(const char *name); diff --git a/src/clonesetup/clonesetup.c b/src/clonesetup/clonesetup.c new file mode 100644 index 0000000000000..c920da618bfb8 --- /dev/null +++ b/src/clonesetup/clonesetup.c @@ -0,0 +1,245 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include +#include /* access */ + +#include "alloc-util.h" +#include "build.h" +#include "clonesetup-ioctl.h" +#include "extract-word.h" +#include "format-table.h" +#include "help-util.h" +#include "log.h" +#include "main-func.h" +#include "options.h" +#include "parse-util.h" +#include "path-util.h" /* path_join */ +#include "string-util.h" +#include "strv.h" /* strv_skip */ +#include "verbs.h" + +/* region_size: size of each dm-clone region in 512-byte sectors. + * Must be a power of 2 between 8 (4 KiB) and 2097152 (1 GiB) per dm-clone kernel docs. */ +#define CLONE_REGION_SIZE_DEFAULT (UINT64_C(1) << 3) /* 8 sectors = 4 KiB */ +#define CLONE_REGION_SIZE_MIN (UINT64_C(1) << 3) /* 8 sectors = 4 KiB */ +#define CLONE_REGION_SIZE_MAX (UINT64_C(1) << 21) /* 2097152 sectors = 1 GiB */ + +static int parse_clone_options(const char *options, uint64_t *ret_region_size) { + uint64_t region_size = CLONE_REGION_SIZE_DEFAULT; + + assert(ret_region_size); + + for (;;) { + _cleanup_free_ char *word = NULL; + const char *val; + int r; + + /* extract_first_word: * + * Returns > 0 — successfully extracted a word * + * Returns 0 — no more words (end of string) * + * Returns < 0 — actual error (e.g. memory allocation failure) */ + r = extract_first_word(&options, &word, ",", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return log_error_errno(r, "Failed to parse clone options: %m"); + if (r == 0) + break; + + /* region_size = N; size of each clone region in 512-byte sectors ( default 8 = 4KB ) + * Must be a power of 2 between 8 and 2097152 per dm-clone kernel docs. */ + /* treat - as empty — common placeholders for "no options" */ + if (streq(word, "-")) + continue; + + if ((val = startswith(word, "region_size="))) { + uint64_t r_size; + r = safe_atou64(val, &r_size); + if (r < 0) + log_warning_errno(r, "Failed to parse region_size= value '%s', using default.", val); + else if (!ISPOWEROF2(r_size) || r_size < CLONE_REGION_SIZE_MIN || r_size > CLONE_REGION_SIZE_MAX) + log_warning("region_size=%s must be a power of two between 8 and 2097152, using default.", val); + else + region_size = r_size; + } else { + /* currently only region_size is supported */ + log_warning("Unknown clone option '%s', ignoring.", word); + } + } + *ret_region_size = region_size; + return 0; +} + +/* dm-clone device creation workflow: + * 1. Create the dm-clone device + * 2. Enable background hydration */ +static int clone_device( + const char *clone_name, + const char *source_dev, + const char *dest_dev, + const char *metadata_dev, + const char *options) { + + _cleanup_free_ char *clone_dev_path = NULL; + int r; + + assert(clone_name); + assert(source_dev); + assert(dest_dev); + assert(metadata_dev); + + /* create clone device path to check if clone device already exists */ + clone_dev_path = path_join("/dev/mapper", clone_name); + if (!clone_dev_path) + return log_oom(); + + /* Check before calling the DM ioctl to give a cleaner error message; + * DM_DEV_CREATE would return EEXIST too, but with a less obvious message. */ + if (access(clone_dev_path, F_OK) >= 0) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device '%s' already exists.", clone_dev_path); + + uint64_t region_size; + r = parse_clone_options(options, ®ion_size); + if (r < 0) + return r; + + r = dm_clone_create_device(clone_name, source_dev, dest_dev, metadata_dev, region_size); + if (r < 0) + return r; + + r = dm_clone_send_message(clone_name, "enable_hydration"); + if (r < 0) { + (void) dm_clone_remove_device_deferred(clone_name); + return r; + } + return 0; +} + +/* Argument validation — what each check covers: + * /, .., leading ., empty name → filename_is_valid() on name + * control chars, \, ', whitespace → string_is_safe() on device paths + * .. in device paths → path_is_normalized() + * non-/dev/ device paths → path_is_absolute() + path_startswith(path, "/dev/") */ +static int validate_dev_path(const char *what, const char *path) { + if (!string_is_safe(path, 0) || !path_is_valid(path) || !path_is_normalized(path) || + !path_is_absolute(path) || !path_startswith(path, "/dev/")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid %s device path '%s'.", what, path); + return 0; +} + +static int validate_fields(const char *name, const char *src, const char *dst, + const char *meta, const char *options) { + if (!filename_is_valid(name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid clone name '%s'.", name); + + int r; + r = validate_dev_path("source", src); + if (r < 0) + return r; + r = validate_dev_path("destination", dst); + if (r < 0) + return r; + r = validate_dev_path("metadata", meta); + if (r < 0) + return r; + if (!string_is_safe(options, 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid options '%s'.", options); + return 0; +} + +VERB(verb_add, "add", "NAME SOURCE DEST METADATA [OPTIONS]", 5, 6, 0, "Create a dm-clone device"); + +/* Arguments: systemd-clonesetup add NAME SOURCE DEST METADATA [OPTIONS] */ +static int verb_add(int argc, char *argv[], uintptr_t data, void *userdata) { + int r; + assert(argc >= 5 && argc <= 6); + + const char *name = ASSERT_PTR(argv[1]); + const char *src = ASSERT_PTR(argv[2]); + const char *dst = ASSERT_PTR(argv[3]); + const char *meta = ASSERT_PTR(argv[4]); + + const char *options = argc == 6 ? argv[5] : ""; + + r = validate_fields(name, src, dst, meta, options); + if (r < 0) + return r; + + log_debug("%s %s %s %s %s opts=%s", __func__, + name, src, dst, meta, options); + return clone_device(name, src, dst, meta, options); +} + +VERB(verb_remove, "remove", "NAME", 2, 2, 0, "Remove a dm-clone device"); + +static int verb_remove(int argc, char *argv[], uintptr_t data, void *userdata) { + const char *name = ASSERT_PTR(argv[1]); + int r; + + r = dm_clone_remove_device(name); + if (r == -ENXIO) { + log_info("Device %s already inactive.", name); + return 0; + } + if (r == -EBUSY) { + r = dm_clone_remove_device_deferred(name); + return r == -ENXIO ? 0 : r; + } + if (r < 0) + return r; + + return 0; +} + +static int help(void) { + _cleanup_(table_unrefp) Table *options = NULL; + int r; + + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + help_cmdline("add NAME SOURCE DEST METADATA [OPTIONS]"); + help_cmdline("remove NAME"); + help_abstract("Add or remove a dm-clone device."); + help_section("Options"); + r = table_print_or_warn(options); + if (r < 0) + return r; + + help_man_page_reference("systemd-clonesetup", "8"); + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + assert(argc >= 0); + assert(argv); + + OptionParser opts = { argc, argv }; + FOREACH_OPTION_OR_RETURN(c, &opts) + switch (c) { + + OPTION_COMMON_HELP: + return help(); + + OPTION_COMMON_VERSION: + return version(); + } + + return 1; +} + +/* systemd-clonesetup uses device-mapper ioctls to create and remove the + * dm-clone devices. */ +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + umask(0022); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return dispatch_verb(strv_skip(argv, 1), /* userdata= */ NULL); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/clonesetup/meson.build b/src/clonesetup/meson.build new file mode 100644 index 0000000000000..47af3060d43e1 --- /dev/null +++ b/src/clonesetup/meson.build @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +systemd_clonesetup_sources = files( + 'clonesetup-ioctl.c', + 'clonesetup.c', +) + +executables += [ + libexec_template + { + 'name' : 'systemd-clonesetup', + 'sources' : systemd_clonesetup_sources, + }, + generator_template + { + 'name' : 'systemd-clonesetup-generator', + 'sources' : files('clonesetup-generator.c'), + }, +] diff --git a/test/integration-tests/TEST-93-CLONESETUP/meson.build b/test/integration-tests/TEST-93-CLONESETUP/meson.build new file mode 100644 index 0000000000000..77370ce4588c4 --- /dev/null +++ b/test/integration-tests/TEST-93-CLONESETUP/meson.build @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +integration_tests += [ + integration_test_template + { + 'name' : fs.name(meson.current_source_dir()), + 'vm' : true, + }, +] diff --git a/test/integration-tests/meson.build b/test/integration-tests/meson.build index 3991b11e79cc6..228d808f51de7 100644 --- a/test/integration-tests/meson.build +++ b/test/integration-tests/meson.build @@ -105,6 +105,7 @@ foreach dirname : [ 'TEST-90-RESTRICT-FSACCESS', 'TEST-91-LIVEUPDATE', 'TEST-92-TPM2-SWTPM', + 'TEST-93-CLONESETUP', ] subdir(dirname) endforeach diff --git a/test/units/TEST-93-CLONESETUP.sh b/test/units/TEST-93-CLONESETUP.sh new file mode 100755 index 0000000000000..1d2ad36c53cb3 --- /dev/null +++ b/test/units/TEST-93-CLONESETUP.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail +set -E + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +CLONESETUP_BIN=/usr/lib/systemd/systemd-clonesetup +# Test clonesetup generator and systemd-clonesetup +# Disable pager usage so interactive/manual runs do not block in `less`. +export SYSTEMD_PAGER=cat +export SYSTEMD_LESS= + +create_loop_triplet() { + local workdir="${1:?}" + local src_img dst_img meta_img + local loop_src loop_dst loop_meta + + src_img="$workdir/source.img" + dst_img="$workdir/dest.img" + meta_img="$workdir/meta.img" + + truncate -s 32M "$src_img" + truncate -s 32M "$dst_img" + truncate -s 8M "$meta_img" + + loop_src="$(losetup --show --find "$src_img")" + loop_dst="$(losetup --show --find "$dst_img")" + loop_meta="$(losetup --show --find "$meta_img")" + + # Wait for udev to process new loop devices before they are consumed. + udevadm settle --timeout=60 + + printf '%s %s %s\n' "$loop_src" "$loop_dst" "$loop_meta" +} + +cleanup_loop_triplet() { + local loop_src="${1:-}" + local loop_dst="${2:-}" + local loop_meta="${3:-}" + + [[ -n "$loop_src" ]] && losetup -d "$loop_src" + [[ -n "$loop_dst" ]] && losetup -d "$loop_dst" + [[ -n "$loop_meta" ]] && losetup -d "$loop_meta" +} + +at_exit() { + set +e + + rm -f /etc/clonetab + [[ -e /tmp/clonetab.bak ]] && cp -fv /tmp/clonetab.bak /etc/clonetab + dmsetup remove testclonesetup 2>/dev/null || true + + systemctl daemon-reload +} + +at_error() { + local rc="$?" + local source="${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}" + local line="${BASH_LINENO[0]:-0}" + local func="${FUNCNAME[1]:-main}" + + echo "ERROR: rc=$rc at $source:$line ($func): $BASH_COMMAND" >&2 + return "$rc" +} + +trap at_exit EXIT +trap at_error ERR + +clonesetup_start_and_check() { + local volume unit + + volume="${1:?}" + unit="systemd-clonesetup@$volume.service" + + # The unit existence check should always pass + [[ "$(systemctl show -P LoadState "$unit")" == loaded ]] + systemctl list-unit-files "$unit" + + systemctl start "$unit" + # wait for udev to create /dev/mapper/ node after DM device activation + udevadm settle --timeout=10 + systemctl status "$unit" + test -e "/dev/mapper/$volume" + dmsetup status "$volume" + + systemctl stop "$unit" + # wait for udev to finish processing so the device node state is in sync + # before the API returns. + udevadm settle --timeout=10 + test ! -e "/dev/mapper/$volume" +} + +prereq() { + # Skip when kernel lacks dm-clone (CONFIG_DM_CLONE) + modprobe dm_clone 2>/dev/null || true + if [[ ! -d /sys/module/dm_clone ]]; then + echo "no dm-clone" >/skipped + exit 77 + fi + echo "Found required kernel module: dm_clone" +} + +prereq + +# Backup existing clonetab if any +[[ -e /etc/clonetab ]] && cp -fv /etc/clonetab /tmp/clonetab.bak + +# Create test clonetab +clonetab_create() { + local loop_name="${1:?}" + local loop_src="${2:?}" + local loop_dst="${3:?}" + local loop_meta="${4:?}" + local loop_options="${5:?}" + + cat >/etc/clonetab </etc/clonetab </etc/clonetab </etc/clonetab </etc/clonetab </etc/clonetab < Date: Fri, 3 Jul 2026 12:20:05 +0530 Subject: [PATCH 71/71] address PR comments --- man/clonetab.xml | 26 +++++----- man/systemd-clonesetup.xml | 5 +- src/clonesetup/clonesetup-generator.c | 9 +--- src/clonesetup/clonesetup-ioctl.c | 34 +++++++++---- src/clonesetup/clonesetup-util.c | 15 ++++++ src/clonesetup/clonesetup-util.h | 4 ++ src/clonesetup/clonesetup.c | 72 ++++++++------------------- src/clonesetup/meson.build | 2 + test/units/TEST-93-CLONESETUP.sh | 4 +- 9 files changed, 85 insertions(+), 86 deletions(-) create mode 100644 src/clonesetup/clonesetup-util.c create mode 100644 src/clonesetup/clonesetup-util.h diff --git a/man/clonetab.xml b/man/clonetab.xml index 651fa339cd163..a22798dbf7ba8 100644 --- a/man/clonetab.xml +++ b/man/clonetab.xml @@ -63,22 +63,22 @@ - + Controls the granularity of background hydration copying — how much - data is copied at a time. Region size is specified in 512-byte sectors, so - region_size=8 means 8 × 512 = 4KB per region. One region is the - atomic unit dm-clone tracks: it is either fully hydrated (copied to the destination) + data is copied at a time. Region size is specified in bytes (standard suffixes like + K, M, G are supported), and must + correspond to a power of two between 4 KiB and 1 GiB. For example, + region_size=4K or region_size=4096 sets a 4 KiB region size. + + + One region is the atomic unit dm-clone tracks: it is either fully hydrated (copied to the destination) or not, never partially. If a copy is interrupted mid-region, that whole region is retried from scratch on next boot. Smaller regions mean finer progress tracking; - larger regions reduce metadata overhead. Must be a power of two between - 8 and 2097152 sectors. Defaults to - 8 (4KB). For background, see + larger regions reduce metadata overhead. Defaults to 4K. For background, see dm-clone kernel documentation. - Device mapper sectors are always 512 bytes regardless of physical block size - (include/linux/blk_types.h: SECTOR_SIZE = 1 << 9). - + - + @@ -104,8 +104,8 @@ Clone with custom region size - Clone a source device to a destination with a custom region size of 16 sectors: - mydevice /dev/sdb /dev/sdc /dev/sdd region_size=16 + Clone a source device to a destination with a custom region size of 8 KiB: + mydevice /dev/sdb /dev/sdc /dev/sdd region_size=8K diff --git a/man/systemd-clonesetup.xml b/man/systemd-clonesetup.xml index f157b8cc28ffb..c488882db8881 100644 --- a/man/systemd-clonesetup.xml +++ b/man/systemd-clonesetup.xml @@ -57,8 +57,9 @@ clonetab5. Currently, the supported option in OPTIONS is - region_size=N, where - N is measured in 512-byte sectors. + region_size=BYTES, where + BYTES is measured in bytes (standard suffixes like + K, M, G are supported). diff --git a/src/clonesetup/clonesetup-generator.c b/src/clonesetup/clonesetup-generator.c index 00ac696c4ac37..fc885dd7392c5 100644 --- a/src/clonesetup/clonesetup-generator.c +++ b/src/clonesetup/clonesetup-generator.c @@ -3,6 +3,7 @@ #include #include "alloc-util.h" +#include "clonesetup-util.h" #include "dropin.h" #include "errno-util.h" #include "fd-util.h" @@ -160,14 +161,6 @@ static int generate_clone_units( return 0; } -static int validate_dev_path(const char *what, const char *path) { - if (!string_is_safe(path, 0) || !path_is_valid(path) || !path_is_normalized(path) || - !path_is_absolute(path) || !path_startswith(path, "/dev/")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid %s device path '%s'.", what, path); - return 0; -} - /* Field validation — what each check covers: * control chars, \, ', whitespace → string_is_safe() on all fields * / in name → string_is_safe(name, STRING_FILENAME) diff --git a/src/clonesetup/clonesetup-ioctl.c b/src/clonesetup/clonesetup-ioctl.c index 812ce9db9cee9..73c52e6e99770 100644 --- a/src/clonesetup/clonesetup-ioctl.c +++ b/src/clonesetup/clonesetup-ioctl.c @@ -21,7 +21,7 @@ * caller can divide by 512 and pass the sector count to dm_clone_load_table(). */ static int get_size(const char *dev_path, uint64_t *ret_size) { _cleanup_(sd_device_unrefp) sd_device *dev = NULL; - uint64_t size; + uint64_t size, temp_size; int r; assert(dev_path); @@ -36,10 +36,12 @@ static int get_size(const char *dev_path, uint64_t *ret_size) { return log_error_errno(r, "Failed to get device size for '%s': %m", dev_path); /* sysfs 'size' is in 512-byte sectors */ - *ret_size = u64_multiply_safe(size, 512); - if (*ret_size == 0 && size != 0) + temp_size = u64_multiply_safe(size, 512); + if (temp_size == 0 && size != 0) return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Device size overflow for '%s'", dev_path); + + *ret_size = temp_size; return 0; } @@ -69,8 +71,10 @@ static int dm_ioctl_run(const char *name, uint32_t cmd, struct dm_ioctl *data, s r = RET_NERRNO(ioctl(fd, cmd, dm)); if (r < 0) { - if (r == -ENXIO && cmd == DM_DEV_REMOVE) - return log_debug_errno(r, "DM ioctl failed: %m"); + if (r == -ENXIO && cmd == DM_DEV_REMOVE) { + log_full_errno(LOG_DEBUG, r, "Device \"%s\" is already inactive, ignoring: %m", dm->name); + return 0; + } return log_error_errno(r, "DM ioctl failed: %m"); } return 0; @@ -78,10 +82,17 @@ static int dm_ioctl_run(const char *name, uint32_t cmd, struct dm_ioctl *data, s /* First dm ioctl needed to create a device. */ static int dm_clone_create(const char *name) { + int r; assert(name); struct dm_ioctl dm = {}; - return dm_ioctl_run(name, DM_DEV_CREATE, &dm, sizeof(dm)); + r = dm_ioctl_run(name, DM_DEV_CREATE, &dm, sizeof(dm)); + if (r < 0) { + if (r == -EEXIST) + return log_error_errno(r, "Device '/dev/mapper/%s' already exists.", name); + return log_error_errno(r, "Failed to create DM device '%s': %m", name); + } + return 0; } /* Second dm ioctl needed to create a device. */ @@ -115,7 +126,7 @@ static int dm_clone_load_table(const char *name, uint64_t size_sectors, const ch }; strncpy(tgt->target_type, "clone", sizeof(tgt->target_type)); - params_buf = (char *) ((uint8_t *) tgt + ALIGN8(sizeof(struct dm_target_spec))); + params_buf = (char *) tgt + ALIGN8(sizeof(struct dm_target_spec)); memcpy(params_buf, target_params, params_len); return dm_ioctl_run(name, DM_TABLE_LOAD, dm, dm_size); @@ -136,10 +147,10 @@ int dm_clone_create_device( const char *source_dev, const char *dest_dev, const char *metadata_dev, - uint64_t region_size) { + uint64_t region_size_bytes) { _cleanup_free_ char *target_params = NULL; - uint64_t src_dev_size_sectors, src_dev_size; + uint64_t src_dev_size_sectors, src_dev_size, region_size_sectors; int r; assert(name); @@ -160,6 +171,9 @@ int dm_clone_create_device( assert(src_dev_size % 512 == 0); src_dev_size_sectors = src_dev_size / 512; + assert(region_size_bytes % 512 == 0); + region_size_sectors = region_size_bytes / 512; + /* dm-clone target params: [options] * region_size = region size in sectors, configurable via clonetab (default 8 = 4KB regions with * 512-byte sectors) 1 = hydration threshold (regions to hydrate per batch) no_hydration = don't @@ -171,7 +185,7 @@ int dm_clone_create_device( * that the paths in params - metadata_dev, dest_dev, source_dev, and region_size must not contain * spaces, which standard /dev/ paths never do, so the below args do NOT require shell escaping */ if (asprintf(&target_params, "%s %s %s %" PRIu64 " 1 no_hydration", - metadata_dev, dest_dev, source_dev, region_size) < 0) + metadata_dev, dest_dev, source_dev, region_size_sectors) < 0) return log_oom(); r = dm_clone_create(name); diff --git a/src/clonesetup/clonesetup-util.c b/src/clonesetup/clonesetup-util.c new file mode 100644 index 0000000000000..299408a9cef18 --- /dev/null +++ b/src/clonesetup/clonesetup-util.c @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "clonesetup-util.h" +#include "errno-util.h" +#include "log.h" +#include "path-util.h" +#include "string-util.h" + +int validate_dev_path(const char *what, const char *path) { + if (!string_is_safe(path, 0) || !path_is_normalized(path) || + !path_is_absolute(path) || !path_startswith(path, "/dev/")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid %s device path '%s'.", what, path); + return 0; +} diff --git a/src/clonesetup/clonesetup-util.h b/src/clonesetup/clonesetup-util.h new file mode 100644 index 0000000000000..2c2fbf06d72a9 --- /dev/null +++ b/src/clonesetup/clonesetup-util.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int validate_dev_path(const char *what, const char *path); diff --git a/src/clonesetup/clonesetup.c b/src/clonesetup/clonesetup.c index c920da618bfb8..413a6cc34d24c 100644 --- a/src/clonesetup/clonesetup.c +++ b/src/clonesetup/clonesetup.c @@ -1,10 +1,10 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include -#include /* access */ #include "alloc-util.h" #include "build.h" #include "clonesetup-ioctl.h" +#include "clonesetup-util.h" #include "extract-word.h" #include "format-table.h" #include "help-util.h" @@ -12,21 +12,21 @@ #include "main-func.h" #include "options.h" #include "parse-util.h" -#include "path-util.h" /* path_join */ +#include "path-util.h" /* filename_is_valid */ #include "string-util.h" #include "strv.h" /* strv_skip */ #include "verbs.h" -/* region_size: size of each dm-clone region in 512-byte sectors. - * Must be a power of 2 between 8 (4 KiB) and 2097152 (1 GiB) per dm-clone kernel docs. */ -#define CLONE_REGION_SIZE_DEFAULT (UINT64_C(1) << 3) /* 8 sectors = 4 KiB */ -#define CLONE_REGION_SIZE_MIN (UINT64_C(1) << 3) /* 8 sectors = 4 KiB */ -#define CLONE_REGION_SIZE_MAX (UINT64_C(1) << 21) /* 2097152 sectors = 1 GiB */ +/* region_size: size of each dm-clone region in bytes. Handled internally in bytes, but must correspond to a + * power of 2 between 4K and 1G per dm-clone kernel docs. */ +#define CLONE_REGION_SIZE_DEFAULT_BYTES (UINT64_C(1) << 12) /* 4 KiB */ +#define CLONE_REGION_SIZE_MIN_BYTES (UINT64_C(1) << 12) /* 4 KiB */ +#define CLONE_REGION_SIZE_MAX_BYTES (UINT64_C(1) << 30) /* 1 GiB */ -static int parse_clone_options(const char *options, uint64_t *ret_region_size) { - uint64_t region_size = CLONE_REGION_SIZE_DEFAULT; +static int parse_clone_options(const char *options, uint64_t *ret_region_size_bytes) { + uint64_t region_size_bytes = CLONE_REGION_SIZE_DEFAULT_BYTES; - assert(ret_region_size); + assert(ret_region_size_bytes); for (;;) { _cleanup_free_ char *word = NULL; @@ -43,27 +43,26 @@ static int parse_clone_options(const char *options, uint64_t *ret_region_size) { if (r == 0) break; - /* region_size = N; size of each clone region in 512-byte sectors ( default 8 = 4KB ) - * Must be a power of 2 between 8 and 2097152 per dm-clone kernel docs. */ /* treat - as empty — common placeholders for "no options" */ if (streq(word, "-")) continue; if ((val = startswith(word, "region_size="))) { - uint64_t r_size; - r = safe_atou64(val, &r_size); + uint64_t r_size_bytes; + /* parse_size handles suffixes like K, M, G automatically */ + r = parse_size(val, 1024, &r_size_bytes); if (r < 0) log_warning_errno(r, "Failed to parse region_size= value '%s', using default.", val); - else if (!ISPOWEROF2(r_size) || r_size < CLONE_REGION_SIZE_MIN || r_size > CLONE_REGION_SIZE_MAX) - log_warning("region_size=%s must be a power of two between 8 and 2097152, using default.", val); + else if (!ISPOWEROF2(r_size_bytes) || r_size_bytes < CLONE_REGION_SIZE_MIN_BYTES || r_size_bytes > CLONE_REGION_SIZE_MAX_BYTES) + log_warning("region_size=%s must be a power of two between 4K and 1G, using default.", val); else - region_size = r_size; + region_size_bytes = r_size_bytes; } else { /* currently only region_size is supported */ log_warning("Unknown clone option '%s', ignoring.", word); } } - *ret_region_size = region_size; + *ret_region_size_bytes = region_size_bytes; return 0; } @@ -77,7 +76,6 @@ static int clone_device( const char *metadata_dev, const char *options) { - _cleanup_free_ char *clone_dev_path = NULL; int r; assert(clone_name); @@ -85,22 +83,12 @@ static int clone_device( assert(dest_dev); assert(metadata_dev); - /* create clone device path to check if clone device already exists */ - clone_dev_path = path_join("/dev/mapper", clone_name); - if (!clone_dev_path) - return log_oom(); - - /* Check before calling the DM ioctl to give a cleaner error message; - * DM_DEV_CREATE would return EEXIST too, but with a less obvious message. */ - if (access(clone_dev_path, F_OK) >= 0) - return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device '%s' already exists.", clone_dev_path); - - uint64_t region_size; - r = parse_clone_options(options, ®ion_size); + uint64_t region_size_bytes; + r = parse_clone_options(options, ®ion_size_bytes); if (r < 0) return r; - r = dm_clone_create_device(clone_name, source_dev, dest_dev, metadata_dev, region_size); + r = dm_clone_create_device(clone_name, source_dev, dest_dev, metadata_dev, region_size_bytes); if (r < 0) return r; @@ -112,19 +100,6 @@ static int clone_device( return 0; } -/* Argument validation — what each check covers: - * /, .., leading ., empty name → filename_is_valid() on name - * control chars, \, ', whitespace → string_is_safe() on device paths - * .. in device paths → path_is_normalized() - * non-/dev/ device paths → path_is_absolute() + path_startswith(path, "/dev/") */ -static int validate_dev_path(const char *what, const char *path) { - if (!string_is_safe(path, 0) || !path_is_valid(path) || !path_is_normalized(path) || - !path_is_absolute(path) || !path_startswith(path, "/dev/")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid %s device path '%s'.", what, path); - return 0; -} - static int validate_fields(const char *name, const char *src, const char *dst, const char *meta, const char *options) { if (!filename_is_valid(name)) @@ -175,13 +150,8 @@ static int verb_remove(int argc, char *argv[], uintptr_t data, void *userdata) { int r; r = dm_clone_remove_device(name); - if (r == -ENXIO) { - log_info("Device %s already inactive.", name); - return 0; - } if (r == -EBUSY) { - r = dm_clone_remove_device_deferred(name); - return r == -ENXIO ? 0 : r; + return dm_clone_remove_device_deferred(name); } if (r < 0) return r; diff --git a/src/clonesetup/meson.build b/src/clonesetup/meson.build index 47af3060d43e1..1793d2c71669f 100644 --- a/src/clonesetup/meson.build +++ b/src/clonesetup/meson.build @@ -9,9 +9,11 @@ executables += [ libexec_template + { 'name' : 'systemd-clonesetup', 'sources' : systemd_clonesetup_sources, + 'extract' : files('clonesetup-util.c'), }, generator_template + { 'name' : 'systemd-clonesetup-generator', 'sources' : files('clonesetup-generator.c'), + 'objects' : ['systemd-clonesetup'], }, ] diff --git a/test/units/TEST-93-CLONESETUP.sh b/test/units/TEST-93-CLONESETUP.sh index 1d2ad36c53cb3..f80df58ca9c69 100755 --- a/test/units/TEST-93-CLONESETUP.sh +++ b/test/units/TEST-93-CLONESETUP.sh @@ -187,8 +187,8 @@ testcase_region_size() { region_workdir="$(mktemp -d)" read -r region_loop_src region_loop_dst region_loop_meta < <(create_loop_triplet "$region_workdir") - # Custom region_size=16 — dmsetup status must report 16 sectors per region. - $CLONESETUP_BIN add testregion "$region_loop_src" "$region_loop_dst" "$region_loop_meta" region_size=16 + # Custom region_size=8K — verify a non-default bytes value is accepted. + $CLONESETUP_BIN add testregion "$region_loop_src" "$region_loop_dst" "$region_loop_meta" region_size=8K dmsetup info testregion $CLONESETUP_BIN remove testregion