From c32bd8b0dc9796291842fea3491f03544497d817 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Mon, 22 Jun 2026 13:45:05 +0100 Subject: [PATCH 001/106] 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 002/106] 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 003/106] 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 004/106] 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 005/106] 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 006/106] 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 007/106] 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 008/106] 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 009/106] 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 010/106] 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 011/106] 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 012/106] 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 013/106] 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 014/106] 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 015/106] 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 016/106] 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 017/106] 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 018/106] 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 019/106] 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 020/106] 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 021/106] 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 022/106] 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 023/106] 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 024/106] 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 025/106] 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 026/106] 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 027/106] 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 028/106] 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 029/106] 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 030/106] 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 031/106] 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 032/106] 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 033/106] 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 034/106] 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 035/106] 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 036/106] 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 037/106] 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 038/106] 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 039/106] 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 040/106] 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 041/106] 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 042/106] 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 043/106] 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 044/106] 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 045/106] 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 046/106] 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 047/106] 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 048/106] 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 049/106] 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 050/106] 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 051/106] 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 052/106] =?UTF-8?q?sysupdate:=20rename=20read=5Fdefinition?= =?UTF-8?q?s()=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 053/106] 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 054/106] 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 055/106] 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 056/106] 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 057/106] 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 058/106] 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 059/106] 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 060/106] 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 061/106] 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 062/106] 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 063/106] 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 064/106] 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 065/106] 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 066/106] 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 067/106] 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 068/106] 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 069/106] 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 fc077f05d7d492bc618bd6c9cfc14b0e518b1c0d Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Thu, 2 Jul 2026 16:22:36 +0200 Subject: [PATCH 070/106] vmspawn: select coco firmware via fw descriptors Instead of requiring --firmware= to point at a raw .fd image for --coco=sev-snp/tdx, pick a suitable firmware automatically from the QEMU firmware descriptors, requiring the amd-sev-snp/intel-tdx feature. CoCo firmware is stateless (it carries no NVRAM template), so teach find_ovmf_config() to select stateless raw-format firmware via new flags. --firmware= now uniformly takes a firmware descriptor path. Signed-off-by: Paul Meyer --- man/systemd-vmspawn.xml | 54 ++++----- src/vmspawn/test-vmspawn-util.c | 187 ++++++++++++++++++++++++++++++++ src/vmspawn/vmspawn-util.c | 67 ++++++++++-- src/vmspawn/vmspawn-util.h | 14 ++- src/vmspawn/vmspawn.c | 83 +++++++++----- 5 files changed, 344 insertions(+), 61 deletions(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index d8afb9d0e157f..4ef1b18f57628 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -317,8 +317,8 @@ boot. Booting a UKI requires uefi. If the special string list is specified, all discovered firmware definition files are listed. If the special string describe is specified, the UEFI firmware that would be selected (taking - into account) is printed and the program exits. If an empty - string is specified, the option is reset to its default. + and into account) is printed and + the program exits. If an empty string is specified, the option is reset to its default. @@ -350,31 +350,33 @@ no. sev-snp enables AMD SEV-SNP. This requires KVM on an x86_64 host with - SNP-capable hardware and firmware. must point to a raw SNP-built - OVMF .fd image; the standard pflash + NVRAM split is not supported under - SNP, so the firmware is loaded via QEMU's and Secure Boot is - unavailable. Direct kernel boot via is required so that the kernel, - initrd and command line are hashed into the launch measurement - (kernel-hashes=on); booting the kernel off the disk image via the firmware - would leave it outside the measurement. Credentials passed via - or are bundled into a cpio archive appended to the initrd - (mirroring what systemd-stub does for ESP credentials), so they enter the - launch measurement via kernel-hashes=on; the SMBIOS and fw_cfg channels - normally used to deliver credentials are not used because they are unmeasured and would be - discarded by PID1 in confidential guests. This channel is measured but not confidential with - respect to the host or VMM: the initrd (and thus the credentials it carries) is supplied to QEMU - as plaintext and only its hash enters the launch measurement, which guarantees integrity but does - not keep the credentials secret from the host. This requires the guest to run a sufficiently - recent version of systemd (supporting /.extra/system_credentials/). A vTPM, - if attached via , must be treated as untrusted by the guest. + SNP-capable hardware and firmware. A suitable SNP-built OVMF firmware is picked automatically + from the installed QEMU firmware descriptors, by requiring the amd-sev-snp + firmware feature; use with a path to a firmware descriptor file to + select a specific one. Secure Boot is unavailable. Direct kernel boot via + is required so that the kernel, initrd and command line are hashed + into the launch measurement (kernel-hashes=on); booting the kernel off the + disk image via the firmware would leave it outside the measurement. Credentials passed via + or are bundled into a + cpio archive appended to the initrd (mirroring what systemd-stub does for + ESP credentials), so they enter the launch measurement via kernel-hashes=on; + the SMBIOS and fw_cfg channels normally used to deliver credentials are not used because they are + unmeasured and would be discarded by PID1 in confidential guests. This channel is measured but + not confidential with respect to the host or VMM: the initrd (and thus the credentials it + carries) is supplied to QEMU as plaintext and only its hash enters the launch measurement, + which guarantees integrity but does not keep the credentials secret from the host. This requires + the guest to run a sufficiently recent version of systemd (supporting + /.extra/system_credentials/). A vTPM, if attached via + , must be treated as untrusted by the guest. tdx enables Intel TDX. This requires KVM on an x86_64 host with - TDX-capable hardware and a TDX-enabled host kernel. As with sev-snp, - must point to a raw TDX-built OVMF (TDVF) .fd - image, which is loaded via QEMU's (pflash + NVRAM split is not - supported), and the CPU model is fixed to host. Firmware is measured into - MRTD when the TD is built. Secure Boot cannot be enrolled at runtime (there is no writable - NVRAM); its state is fixed by the supplied TDVF image and is part of the measured firmware. + TDX-capable hardware and a TDX-enabled host kernel. As with sev-snp, a + TDX-built OVMF (TDVF) firmware is picked automatically from the installed QEMU firmware + descriptors, by requiring the intel-tdx firmware feature; use + with a path to a firmware descriptor file to select a specific + one. The CPU model is fixed to host. Firmware is measured into MRTD when the + TD is built. Secure Boot cannot be enrolled at runtime (there is no writable NVRAM); its state + is fixed by the selected TDVF image and is part of the measured firmware. When booting a UKI, the whole UKI PE is measured into RTMR 1, and the loaded sections are measured individually by systemd-stub into RTMR 2. For direct linux boot, firmware measures the kernel PE into RTMR 1, and the Linux EFI stub measures initrd and @@ -384,7 +386,7 @@ to the host or VMM, since the host assembles the SMBIOS table. A vTPM, if attached via , must be treated as untrusted by the guest. To obtain TD Quotes for remote attestation, the guest is wired to the host's local TDX Quote Generation Service - automatically: the unix socket up /run/tdx-qgs/qgs.socket is used if it + automatically: the unix socket /run/tdx-qgs/qgs.socket is used if it exists, otherwise vsock port 4050 on the host (cid 2). If the QGS is listening on neither channel, the guest's quote requests will fail. diff --git a/src/vmspawn/test-vmspawn-util.c b/src/vmspawn/test-vmspawn-util.c index d487cbcacb781..d5108a73d7d7f 100644 --- a/src/vmspawn/test-vmspawn-util.c +++ b/src/vmspawn/test-vmspawn-util.c @@ -1,8 +1,17 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + +#include "sd-json.h" + #include "alloc-util.h" +#include "fileio.h" +#include "path-util.h" +#include "rm-rf.h" +#include "set.h" #include "string-util.h" #include "tests.h" +#include "tmpfile-util.h" #include "vmspawn-util.h" #define _ESCAPE_QEMU_VALUE_CHECK(str, correct, varname) \ @@ -23,4 +32,182 @@ TEST(escape_qemu_value) { ESCAPE_QEMU_VALUE_CHECK("", ""); } +typedef enum TestMapping { + MAPPING_FLASH_SPLIT, /* pflash executable + NVRAM template */ + MAPPING_FLASH_STATELESS, /* read-only pflash, no NVRAM */ + MAPPING_FLASH_COMBINED, /* read-write pflash with the variable store inside the executable */ + MAPPING_MEMORY, /* mapped into memory, loaded via -bios */ + MAPPING_KERNEL, /* loaded like a Linux kernel */ +} TestMapping; + +static void write_descriptor( + const char *dir, + const char *name, + const char *executable, + const char *format, + const char *interface_type, + const char *arch, + const char *machine, + TestMapping mapping, + char **features) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + _cleanup_free_ char *j = NULL, *p = NULL; + + if (!arch) + ASSERT_OK(native_arch_as_qemu(&arch)); + + bool flash = IN_SET(mapping, MAPPING_FLASH_SPLIT, MAPPING_FLASH_STATELESS, MAPPING_FLASH_COMBINED); + + ASSERT_OK(sd_json_buildo(&v, + SD_JSON_BUILD_PAIR_STRING("description", name), + SD_JSON_BUILD_PAIR_STRV("interface-types", STRV_MAKE(interface_type ?: "uefi")), + SD_JSON_BUILD_PAIR("mapping", SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_STRING("device", flash ? "flash" : mapping == MAPPING_MEMORY ? "memory" : "kernel"), + SD_JSON_BUILD_PAIR_CONDITION(!flash, "filename", SD_JSON_BUILD_STRING(executable)), + SD_JSON_BUILD_PAIR_CONDITION(mapping == MAPPING_FLASH_STATELESS, "mode", SD_JSON_BUILD_STRING("stateless")), + SD_JSON_BUILD_PAIR_CONDITION(mapping == MAPPING_FLASH_COMBINED, "mode", SD_JSON_BUILD_STRING("combined")), + SD_JSON_BUILD_PAIR_CONDITION(mapping == MAPPING_FLASH_SPLIT, "nvram-template", SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_STRING("filename", "/test/vars.fd"), + SD_JSON_BUILD_PAIR_STRING("format", "raw"))), + SD_JSON_BUILD_PAIR_CONDITION(flash, "executable", SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_STRING("filename", executable), + SD_JSON_BUILD_PAIR_STRING("format", format))))), + SD_JSON_BUILD_PAIR("targets", SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_STRING("architecture", arch), + SD_JSON_BUILD_PAIR_STRV("machines", STRV_MAKE(machine ?: QEMU_MACHINE_TYPE))))), + SD_JSON_BUILD_PAIR_STRV("features", features), + SD_JSON_BUILD_PAIR_EMPTY_ARRAY("tags"))); + + ASSERT_OK(sd_json_variant_format(v, /* flags= */ 0, &j)); + ASSERT_NOT_NULL(p = path_join(dir, name)); + ASSERT_OK(write_string_file(p, j, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755)); +} + +/* Searches with the given feature sets and flags, and asserts that the firmware with the expected + * executable path is selected, or -ENOENT if expect_path is NULL. Optionally returns the selected config + * for further assertions. */ +static void check_find(char **include, char **exclude, FindOvmfConfigFlags flags, const char *expect_path, OvmfConfig **ret) { + _cleanup_set_free_ Set *inc = NULL, *exc = NULL; + _cleanup_(ovmf_config_freep) OvmfConfig *config = NULL; + int r; + + ASSERT_OK(set_put_strdupv(&inc, include)); + ASSERT_OK(set_put_strdupv(&exc, exclude)); + + r = find_ovmf_config(inc, exc, flags, &config, /* ret_firmware_json= */ NULL); + if (!expect_path) + ASSERT_ERROR(r, ENOENT); + else { + ASSERT_OK(r); + ASSERT_STREQ(config->path, expect_path); + } + + if (ret) + *ret = TAKE_PTR(config); +} + +TEST(find_ovmf_config) { + _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL; + _cleanup_(ovmf_config_freep) OvmfConfig *config = NULL; + _cleanup_free_ char *dir = NULL; + + if (native_arch_as_qemu(/* ret= */ NULL) < 0) + return (void) log_tests_skipped("native architecture not supported by qemu"); + + ASSERT_OK(mkdtemp_malloc("/tmp/test-vmspawn-firmware-XXXXXX", &tmp)); + ASSERT_OK_ERRNO(setenv("XDG_CONFIG_HOME", tmp, /* overwrite= */ true)); + ASSERT_NOT_NULL(dir = path_join(tmp, "qemu/firmware")); + + /* All fixtures declare made-up features and every search below requires one of them, so + * descriptors installed on the host can never match and the test stays hermetic. */ + + /* Stateful vs. stateless selection, in both sort orders. */ + write_descriptor(dir, "00-a-stateless.json", "/test/a-stateless.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_STATELESS, STRV_MAKE("vmspawn-test-a")); + write_descriptor(dir, "10-a-stateful.json", "/test/a-stateful.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-a")); + write_descriptor(dir, "00-b-stateful.json", "/test/b-stateful.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-b")); + write_descriptor(dir, "10-b-stateless.json", "/test/b-stateless.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_STATELESS, STRV_MAKE("vmspawn-test-b")); + + /* By default only firmware with an NVRAM template is considered. */ + check_find(STRV_MAKE("vmspawn-test-a"), /* exclude= */ NULL, /* flags= */ 0, "/test/a-stateful.fd", &config); + ASSERT_STREQ(config->vars, "/test/vars.fd"); + ASSERT_STREQ(config->format, "raw"); + ASSERT_FALSE(ovmf_config_is_stateless(config)); + ASSERT_FALSE(config->supports_sb); + config = ovmf_config_free(config); + + /* With FIND_OVMF_STATELESS only firmware in stateless flash mode is considered. */ + check_find(STRV_MAKE("vmspawn-test-a"), /* exclude= */ NULL, FIND_OVMF_STATELESS, "/test/a-stateless.fd", &config); + ASSERT_NULL(config->vars); + ASSERT_TRUE(ovmf_config_is_stateless(config)); + config = ovmf_config_free(config); + + check_find(STRV_MAKE("vmspawn-test-b"), /* exclude= */ NULL, /* flags= */ 0, "/test/b-stateful.fd", /* ret= */ NULL); + check_find(STRV_MAKE("vmspawn-test-b"), /* exclude= */ NULL, FIND_OVMF_STATELESS, "/test/b-stateless.fd", /* ret= */ NULL); + + /* FIND_OVMF_REQUIRE_RAW skips firmware in other formats, which is accepted otherwise. */ + write_descriptor(dir, "00-c-qcow2.json", "/test/c.qcow2", "qcow2", NULL, NULL, NULL, MAPPING_FLASH_STATELESS, STRV_MAKE("vmspawn-test-c")); + write_descriptor(dir, "10-c-raw.json", "/test/c.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_STATELESS, STRV_MAKE("vmspawn-test-c")); + + check_find(STRV_MAKE("vmspawn-test-c"), /* exclude= */ NULL, FIND_OVMF_STATELESS, "/test/c.qcow2", &config); + ASSERT_STREQ(config->format, "qcow2"); + config = ovmf_config_free(config); + + check_find(STRV_MAKE("vmspawn-test-c"), /* exclude= */ NULL, FIND_OVMF_STATELESS|FIND_OVMF_REQUIRE_RAW, "/test/c.fd", /* ret= */ NULL); + + /* Memory-mapped firmware (loaded via -bios) is stateless and raw by definition. */ + write_descriptor(dir, "00-m-memory.json", "/test/m-memory.fd", "raw", NULL, NULL, NULL, MAPPING_MEMORY, STRV_MAKE("vmspawn-test-m")); + + check_find(STRV_MAKE("vmspawn-test-m"), /* exclude= */ NULL, /* flags= */ 0, /* expect_path= */ NULL, /* ret= */ NULL); + check_find(STRV_MAKE("vmspawn-test-m"), /* exclude= */ NULL, FIND_OVMF_STATELESS|FIND_OVMF_REQUIRE_RAW, "/test/m-memory.fd", &config); + ASSERT_NULL(config->vars); + ASSERT_NULL(config->format); + ASSERT_STREQ(ovmf_config_format(config), "raw"); + ASSERT_TRUE(ovmf_config_is_stateless(config)); + config = ovmf_config_free(config); + + /* Combined-mode flash firmware carries a writable variable store inside the executable, it must not be treated as stateless. */ + write_descriptor(dir, "00-f-combined.json", "/test/f-combined.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_COMBINED, STRV_MAKE("vmspawn-test-f")); + + check_find(STRV_MAKE("vmspawn-test-f"), /* exclude= */ NULL, /* flags= */ 0, /* expect_path= */ NULL, /* ret= */ NULL); + check_find(STRV_MAKE("vmspawn-test-f"), /* exclude= */ NULL, FIND_OVMF_STATELESS, /* expect_path= */ NULL, /* ret= */ NULL); + + /* Firmware that is not UEFI, uses an unsupported mapping device, or doesn't match the native + * architecture or machine type, is skipped. */ + write_descriptor(dir, "00-d-bios.json", "/test/d-bios.fd", "raw", "bios", NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-d")); + write_descriptor(dir, "05-d-kernel.json", "/test/d-kernel.fd", "raw", NULL, NULL, NULL, MAPPING_KERNEL, STRV_MAKE("vmspawn-test-d")); + write_descriptor(dir, "10-d-arch.json", "/test/d-arch.fd", "raw", NULL, "vmspawn-test-arch", NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-d")); + write_descriptor(dir, "20-d-machine.json", "/test/d-machine.fd", "raw", NULL, NULL, "vmspawn-test-mach", MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-d")); + write_descriptor(dir, "30-d-good.json", "/test/d-good.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-d")); + + check_find(STRV_MAKE("vmspawn-test-d"), /* exclude= */ NULL, /* flags= */ 0, "/test/d-good.fd", /* ret= */ NULL); + + /* Feature include/exclude handling. */ + write_descriptor(dir, "00-e-both.json", "/test/e-both.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-e1", "vmspawn-test-e2", "secure-boot")); + write_descriptor(dir, "10-e-one.json", "/test/e-one.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-e1")); + + /* The first matching descriptor in sort order wins. */ + check_find(STRV_MAKE("vmspawn-test-e1"), /* exclude= */ NULL, /* flags= */ 0, "/test/e-both.fd", &config); + ASSERT_TRUE(config->supports_sb); + config = ovmf_config_free(config); + + /* Descriptors with an excluded feature are skipped. */ + check_find(STRV_MAKE("vmspawn-test-e1"), STRV_MAKE("vmspawn-test-e2"), /* flags= */ 0, "/test/e-one.fd", /* ret= */ NULL); + + /* Inclusion wins over exclusion. */ + check_find(STRV_MAKE("vmspawn-test-e1", "vmspawn-test-e2"), STRV_MAKE("vmspawn-test-e2"), /* flags= */ 0, "/test/e-both.fd", /* ret= */ NULL); + + /* All included features must be present. */ + check_find(STRV_MAKE("vmspawn-test-e1", "vmspawn-test-nonexistent"), /* exclude= */ NULL, /* flags= */ 0, /* expect_path= */ NULL, /* ret= */ NULL); + + /* The firmware description JSON is returned on request. */ + _cleanup_set_free_ Set *inc = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; + ASSERT_OK(set_put_strdup(&inc, "vmspawn-test-a")); + ASSERT_OK(find_ovmf_config(inc, /* features_exclude= */ NULL, /* flags= */ 0, &config, &json)); + ASSERT_TRUE(sd_json_variant_is_object(json)); + + ASSERT_OK_ERRNO(unsetenv("XDG_CONFIG_HOME")); +} + DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/vmspawn/vmspawn-util.c b/src/vmspawn/vmspawn-util.c index 8ecd26600064e..5b0ced8e01e04 100644 --- a/src/vmspawn/vmspawn-util.c +++ b/src/vmspawn/vmspawn-util.c @@ -43,7 +43,7 @@ static const char* const architecture_to_qemu_table[_ARCHITECTURE_MAX] = { [ARCHITECTURE_S390X] = "s390x", }; -static int native_arch_as_qemu(const char **ret) { +int native_arch_as_qemu(const char **ret) { const char *s = architecture_to_qemu_table[native_architecture()]; if (!s) return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Architecture %s not supported by qemu", architecture_to_string(native_architecture())); @@ -62,9 +62,28 @@ OvmfConfig* ovmf_config_free(OvmfConfig *config) { free(config->format); free(config->vars); free(config->vars_format); + free(config->device); + free(config->mode); return mfree(config); } +static bool firmware_is_stateless(const char *device, const char *mode) { + /* Memory-mapped firmware (loaded via -bios) carries no NVRAM. For flash, only mode "stateless" + * is stateless: an absent mode means "split" (executable plus NVRAM template), and "combined" + * images carry the variable store inside the (writable) executable. */ + + if (streq_ptr(device, "memory")) + return true; + + return streq_ptr(device, "flash") && streq_ptr(mode, "stateless"); +} + +bool ovmf_config_is_stateless(const OvmfConfig *config) { + assert(config); + + return firmware_is_stateless(config->device, config->mode); +} + DEFINE_STRING_TABLE_LOOKUP(network_stack, NetworkStack); int qemu_check_kvm_support(void) { @@ -140,6 +159,8 @@ typedef struct FirmwareData { char *firmware_format; char *vars; char *vars_format; + char *device; + char *mode; FirmwareTarget **targets; size_t n_targets; } FirmwareData; @@ -180,6 +201,8 @@ static FirmwareData* firmware_data_free(FirmwareData *fwd) { free(fwd->firmware_format); free(fwd->vars); free(fwd->vars_format); + free(fwd->device); + free(fwd->mode); firmware_target_free_many(fwd->targets, fwd->n_targets); return mfree(fwd); @@ -207,14 +230,30 @@ static int firmware_nvram_template(const char *name, sd_json_variant *v, sd_json } static int firmware_mapping(const char *name, sd_json_variant *v, sd_json_dispatch_flags_t flags, void *userdata) { - static const sd_json_dispatch_field table[] = { - { "device", SD_JSON_VARIANT_STRING, NULL, 0, SD_JSON_MANDATORY }, - { "executable", SD_JSON_VARIANT_OBJECT, firmware_executable, 0, SD_JSON_MANDATORY }, - { "nvram-template", SD_JSON_VARIANT_OBJECT, firmware_nvram_template, 0, 0 }, + static const sd_json_dispatch_field table_flash[] = { + { "device", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(FirmwareData, device), SD_JSON_MANDATORY }, + { "mode", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(FirmwareData, mode), 0 }, + { "executable", SD_JSON_VARIANT_OBJECT, firmware_executable, 0, SD_JSON_MANDATORY }, + { "nvram-template", SD_JSON_VARIANT_OBJECT, firmware_nvram_template, 0, 0 }, + {} + }; + static const sd_json_dispatch_field table_memory[] = { + { "device", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(FirmwareData, device), SD_JSON_MANDATORY }, + { "filename", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(FirmwareData, firmware), SD_JSON_MANDATORY }, {} }; - return sd_json_dispatch(v, table, flags, userdata); + sd_json_variant *d = sd_json_variant_by_key(v, "device"); + if (!d || !sd_json_variant_is_string(d)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Firmware mapping lacks a device type."); + + const char *device = sd_json_variant_string(d); + if (streq(device, "flash")) + return sd_json_dispatch(v, table_flash, flags, userdata); + if (streq(device, "memory")) + return sd_json_dispatch(v, table_memory, flags, userdata); + + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported firmware mapping device type '%s'.", device); } static int dispatch_targets(const char *name, sd_json_variant *v, sd_json_dispatch_flags_t flags, void *userdata) { @@ -406,6 +445,8 @@ static int ovmf_config_make(FirmwareData *fwd, OvmfConfig **ret) { .format = TAKE_PTR(fwd->firmware_format), .vars = TAKE_PTR(fwd->vars), .vars_format = TAKE_PTR(fwd->vars_format), + .device = TAKE_PTR(fwd->device), + .mode = TAKE_PTR(fwd->mode), .supports_sb = firmware_data_supports_sb(fwd), }; @@ -430,6 +471,7 @@ int load_ovmf_config(const char *path, OvmfConfig **ret) { int find_ovmf_config( Set *features_include, Set *features_exclude, + FindOvmfConfigFlags flags, OvmfConfig **ret, sd_json_variant **ret_firmware_json) { _cleanup_(ovmf_config_freep) OvmfConfig *config = NULL; @@ -465,11 +507,22 @@ int find_ovmf_config( continue; } - if (!fwd->vars) { + if (FLAGS_SET(flags, FIND_OVMF_STATELESS)) { + if (!firmware_is_stateless(fwd->device, fwd->mode)) { + log_debug("Skipping %s, firmware is not stateless.", *file); + continue; + } + } else if (!fwd->vars) { log_debug("Skipping %s, firmware does not have an NVRAM template.", *file); continue; } + /* Memory-mapped firmware carries no format field and is raw by definition. */ + if (FLAGS_SET(flags, FIND_OVMF_REQUIRE_RAW) && !streq(fwd->firmware_format ?: "raw", "raw")) { + log_debug("Skipping %s, firmware image is not in raw format.", *file); + continue; + } + /* Check if any target matches our architecture and machine type. Machine * patterns in firmware descriptions use globs like "pc-q35-*", so we do a * substring check to see if our machine type (e.g. "q35") appears in any of diff --git a/src/vmspawn/vmspawn-util.h b/src/vmspawn/vmspawn-util.h index 38bb331dfc340..cc7cd147aa677 100644 --- a/src/vmspawn/vmspawn-util.h +++ b/src/vmspawn/vmspawn-util.h @@ -99,6 +99,8 @@ typedef struct OvmfConfig { char *format; char *vars; char *vars_format; + char *device; + char *mode; bool supports_sb; } OvmfConfig; @@ -109,6 +111,7 @@ static inline const char* ovmf_config_format(const OvmfConfig *c) { static inline const char* ovmf_config_vars_format(const OvmfConfig *c) { return ASSERT_PTR(c)->vars_format ?: "raw"; } +bool ovmf_config_is_stateless(const OvmfConfig *config); OvmfConfig* ovmf_config_free(OvmfConfig *ovmf_config); DEFINE_TRIVIAL_CLEANUP_FUNC(OvmfConfig*, ovmf_config_free); @@ -134,7 +137,16 @@ int qemu_check_vsock_support(void); int list_ovmf_config(char ***ret); int list_ovmf_firmware_features(char ***ret); int load_ovmf_config(const char *path, OvmfConfig **ret); -int find_ovmf_config(Set *features_include, Set *features_exclude, OvmfConfig **ret, sd_json_variant **ret_firmware_json); + +typedef enum FindOvmfConfigFlags { + FIND_OVMF_STATELESS = 1 << 0, /* select stateless firmware (flash mode "stateless" or memory-mapped, + * bootable via -bios); default selects pflash firmware with an NVRAM + * template */ + FIND_OVMF_REQUIRE_RAW = 1 << 1, /* skip non-raw firmware executables (-bios can't load qcow2) */ +} FindOvmfConfigFlags; + +int find_ovmf_config(Set *features_include, Set *features_exclude, FindOvmfConfigFlags flags, OvmfConfig **ret, sd_json_variant **ret_firmware_json); +int native_arch_as_qemu(const char **ret); int find_qemu_binary(char **ret_qemu_binary); int vsock_fix_child_cid(int vhost_device_fd, unsigned *machine_cid, const char *machine); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 00df10874ca02..e5fb02a234825 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -79,6 +79,7 @@ #include "socket-util.h" #include "stat-util.h" #include "stdio-util.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" #include "swtpm-util.h" @@ -2552,6 +2553,45 @@ static int prepare_device_info(const char *runtime_dir, MachineConfig *c) { return assign_pcie_ports(c); } +/* Maps a confidential computing mode to the firmware descriptor feature that firmware must declare + * to support it. Note that the feature names are defined by the QEMU firmware interop spec and + * deviate slightly from our own names for the same modes (cf. confidential_computing_to_string()): + * "sev-snp" vs. "amd-sev-snp", and "tdx" vs. "intel-tdx". */ +static const char* const coco_firmware_feature_table[_COCO_MAX] = { + [COCO_AMD_SEV_SNP] = "amd-sev-snp", + [COCO_INTEL_TDX] = "intel-tdx", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(coco_firmware_feature, ConfidentialComputing); + +static int discover_ovmf_config(OvmfConfig **ret, sd_json_variant **ret_firmware_json) { + int r; + + assert(ret); + + const char *coco_feature = coco_firmware_feature_to_string(arg_confidential_computing); + if (coco_feature) { + r = set_put_strdup(&arg_firmware_features_include, coco_feature); + if (r < 0) + return log_oom(); + } + + FindOvmfConfigFlags flags = + arg_confidential_computing != COCO_NO ? FIND_OVMF_STATELESS|FIND_OVMF_REQUIRE_RAW : + 0; + + r = find_ovmf_config(arg_firmware_features_include, arg_firmware_features_exclude, flags, ret, ret_firmware_json); + if (r == -ENOENT && coco_feature) + return log_error_errno(r, "No suitable firmware descriptor found for --coco=%s " + "(requires stateless firmware in raw format with the '%s' firmware feature). " + "Install a suitable firmware or select a firmware descriptor with --firmware=.", + confidential_computing_to_string(arg_confidential_computing), coco_feature); + if (r < 0) + return log_error_errno(r, "Failed to find OVMF config: %m"); + + return 0; +} + static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL; _cleanup_free_ char *qemu_binary = NULL, *mem = NULL; @@ -2618,13 +2658,16 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--coco= requires KVM, but KVM is not available."); - if (arg_firmware_type == FIRMWARE_UEFI && arg_confidential_computing == COCO_NO) { - if (arg_firmware) + if (arg_firmware_type == FIRMWARE_UEFI) { + if (arg_firmware) { r = load_ovmf_config(arg_firmware, &ovmf_config); - else - r = find_ovmf_config(arg_firmware_features_include, arg_firmware_features_exclude, &ovmf_config, /* ret_firmware_json= */ NULL); - if (r < 0) - return log_error_errno(r, "Failed to find OVMF config: %m"); + if (r < 0) + return log_error_errno(r, "Failed to load firmware descriptor '%s': %m", arg_firmware); + } else { + r = discover_ovmf_config(&ovmf_config, /* ret_firmware_json= */ NULL); + if (r < 0) + return r; + } if (set_contains(arg_firmware_features_include, "secure-boot") && !ovmf_config->supports_sb) return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), @@ -2698,7 +2741,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return r; } - if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM) { + if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM && arg_confidential_computing == COCO_NO) { r = qemu_config_key(config_file, "smm", on_off(ovmf_config->supports_sb)); if (r < 0) return r; @@ -3141,9 +3184,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { } } + /* Memory-mapped firmware asks for -bios loading by definition. Under + * confidential computing -bios is used even for (stateless) flash firmware. */ _cleanup_(unlink_and_freep) char *ovmf_vars = NULL; - if (arg_confidential_computing != COCO_NO) { - r = strv_extend_many(&cmdline, "-bios", arg_firmware); + if (ovmf_config && (arg_confidential_computing != COCO_NO || streq_ptr(ovmf_config->device, "memory"))) { + r = strv_extend_many(&cmdline, "-bios", ovmf_config->path); if (r < 0) return r; } else { @@ -4178,14 +4223,6 @@ static int verify_arguments(void) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--coco=sev-snp can't be used with %s firmware", firmware_to_string(arg_firmware_type)); - /* SNP can't use pflash + NVRAM split, so the firmware-descriptor - * machinery doesn't apply. Require an explicit raw .fd path and - * use it verbatim with -bios later. */ - if (!arg_firmware) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--coco=sev-snp requires --firmware=PATH " - "pointing at a raw SNP-built OVMF .fd binary."); - log_debug("Using raw SNP firmware at %s (no NVRAM, no Secure Boot).", arg_firmware); if (set_contains(arg_firmware_features_include, "secure-boot")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--coco=sev-snp cannot be combined with --secure-boot=yes."); @@ -4212,14 +4249,6 @@ static int verify_arguments(void) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--coco=tdx can't be used with %s firmware", firmware_to_string(arg_firmware_type)); - /* TDX can't use pflash + NVRAM split, so the firmware-descriptor - * machinery doesn't apply. Require an explicit raw .fd path and - * use it verbatim with -bios later. */ - if (!arg_firmware) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--coco=tdx requires --firmware=PATH " - "pointing at a raw TDX-built OVMF (TDVF) .fd binary."); - log_debug("Using raw TDX firmware at %s (no NVRAM, no Secure Boot).", arg_firmware); /* Secure Boot state is baked into the supplied TDVF image and can't be enrolled at * runtime (no writable NVRAM), so --secure-boot=yes would silently have no effect. */ if (set_contains(arg_firmware_features_include, "secure-boot")) @@ -4252,9 +4281,9 @@ static int run(int argc, char *argv[]) { _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; - r = find_ovmf_config(arg_firmware_features_include, arg_firmware_features_exclude, &ovmf_config, &json); + r = discover_ovmf_config(&ovmf_config, &json); if (r < 0) - return log_error_errno(r, "Failed to find OVMF config: %m"); + return r; r = sd_json_variant_dump(json, SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_COLOR_AUTO, stdout, /* prefix= */ NULL); if (r < 0) From 2f95c17a2f3f3e564b3bfca31e7f2c035417cc00 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Thu, 2 Jul 2026 18:04:36 +0200 Subject: [PATCH 071/106] vmspawn: verify selected fw matches requirements Carry the firmware descriptor's feature list in OvmfConfig, and verify that firmware selected via an explicit --firmware= descriptor declares the amd-sev-snp/intel-tdx feature, is stateless, and is in raw format, the same constraints automatic discovery already imposes. This replaces the cached supports_sb boolean with a generic feature lookup. Signed-off-by: Paul Meyer --- src/vmspawn/test-vmspawn-util.c | 14 ++++++++---- src/vmspawn/vmspawn-util.c | 16 ++++++++------ src/vmspawn/vmspawn-util.h | 4 +++- src/vmspawn/vmspawn.c | 38 ++++++++++++++++++++++++++++++--- 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/vmspawn/test-vmspawn-util.c b/src/vmspawn/test-vmspawn-util.c index d5108a73d7d7f..21d22e9161a24 100644 --- a/src/vmspawn/test-vmspawn-util.c +++ b/src/vmspawn/test-vmspawn-util.c @@ -133,7 +133,7 @@ TEST(find_ovmf_config) { ASSERT_STREQ(config->vars, "/test/vars.fd"); ASSERT_STREQ(config->format, "raw"); ASSERT_FALSE(ovmf_config_is_stateless(config)); - ASSERT_FALSE(config->supports_sb); + ASSERT_FALSE(ovmf_config_has_feature(config, "secure-boot")); config = ovmf_config_free(config); /* With FIND_OVMF_STATELESS only firmware in stateless flash mode is considered. */ @@ -188,14 +188,20 @@ TEST(find_ovmf_config) { /* The first matching descriptor in sort order wins. */ check_find(STRV_MAKE("vmspawn-test-e1"), /* exclude= */ NULL, /* flags= */ 0, "/test/e-both.fd", &config); - ASSERT_TRUE(config->supports_sb); + ASSERT_TRUE(ovmf_config_has_feature(config, "secure-boot")); config = ovmf_config_free(config); /* Descriptors with an excluded feature are skipped. */ - check_find(STRV_MAKE("vmspawn-test-e1"), STRV_MAKE("vmspawn-test-e2"), /* flags= */ 0, "/test/e-one.fd", /* ret= */ NULL); + check_find(STRV_MAKE("vmspawn-test-e1"), STRV_MAKE("vmspawn-test-e2"), /* flags= */ 0, "/test/e-one.fd", &config); + ASSERT_TRUE(ovmf_config_has_feature(config, "vmspawn-test-e1")); + ASSERT_FALSE(ovmf_config_has_feature(config, "vmspawn-test-e2")); + config = ovmf_config_free(config); /* Inclusion wins over exclusion. */ - check_find(STRV_MAKE("vmspawn-test-e1", "vmspawn-test-e2"), STRV_MAKE("vmspawn-test-e2"), /* flags= */ 0, "/test/e-both.fd", /* ret= */ NULL); + check_find(STRV_MAKE("vmspawn-test-e1", "vmspawn-test-e2"), STRV_MAKE("vmspawn-test-e2"), /* flags= */ 0, "/test/e-both.fd", &config); + ASSERT_TRUE(ovmf_config_has_feature(config, "vmspawn-test-e1")); + ASSERT_TRUE(ovmf_config_has_feature(config, "vmspawn-test-e2")); + config = ovmf_config_free(config); /* All included features must be present. */ check_find(STRV_MAKE("vmspawn-test-e1", "vmspawn-test-nonexistent"), /* exclude= */ NULL, /* flags= */ 0, /* expect_path= */ NULL, /* ret= */ NULL); diff --git a/src/vmspawn/vmspawn-util.c b/src/vmspawn/vmspawn-util.c index 5b0ced8e01e04..03fef0f1b8b9c 100644 --- a/src/vmspawn/vmspawn-util.c +++ b/src/vmspawn/vmspawn-util.c @@ -64,9 +64,17 @@ OvmfConfig* ovmf_config_free(OvmfConfig *config) { free(config->vars_format); free(config->device); free(config->mode); + strv_free(config->features); return mfree(config); } +bool ovmf_config_has_feature(const OvmfConfig *config, const char *feature) { + assert(config); + assert(feature); + + return strv_contains(config->features, feature); +} + static bool firmware_is_stateless(const char *device, const char *mode) { /* Memory-mapped firmware (loaded via -bios) carries no NVRAM. For flash, only mode "stateless" * is stateless: an absent mode means "split" (executable plus NVRAM template), and "combined" @@ -185,12 +193,6 @@ static bool firmware_data_matches_machine(const FirmwareData *fwd, const char *a return false; } -static bool firmware_data_supports_sb(const FirmwareData *fwd) { - assert(fwd); - - return strv_contains(fwd->features, "secure-boot"); -} - static FirmwareData* firmware_data_free(FirmwareData *fwd) { if (!fwd) return NULL; @@ -447,7 +449,7 @@ static int ovmf_config_make(FirmwareData *fwd, OvmfConfig **ret) { .vars_format = TAKE_PTR(fwd->vars_format), .device = TAKE_PTR(fwd->device), .mode = TAKE_PTR(fwd->mode), - .supports_sb = firmware_data_supports_sb(fwd), + .features = TAKE_PTR(fwd->features), }; *ret = TAKE_PTR(config); diff --git a/src/vmspawn/vmspawn-util.h b/src/vmspawn/vmspawn-util.h index cc7cd147aa677..8d1911663ca1c 100644 --- a/src/vmspawn/vmspawn-util.h +++ b/src/vmspawn/vmspawn-util.h @@ -101,7 +101,7 @@ typedef struct OvmfConfig { char *vars_format; char *device; char *mode; - bool supports_sb; + char **features; } OvmfConfig; static inline const char* ovmf_config_format(const OvmfConfig *c) { @@ -111,7 +111,9 @@ static inline const char* ovmf_config_format(const OvmfConfig *c) { static inline const char* ovmf_config_vars_format(const OvmfConfig *c) { return ASSERT_PTR(c)->vars_format ?: "raw"; } + bool ovmf_config_is_stateless(const OvmfConfig *config); +bool ovmf_config_has_feature(const OvmfConfig *config, const char *feature); OvmfConfig* ovmf_config_free(OvmfConfig *ovmf_config); DEFINE_TRIVIAL_CLEANUP_FUNC(OvmfConfig*, ovmf_config_free); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index e5fb02a234825..2121fd29d0b04 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2669,11 +2669,42 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return r; } - if (set_contains(arg_firmware_features_include, "secure-boot") && !ovmf_config->supports_sb) + /* Flash mode "combined" places the variable store in the (writable) executable, + * which would have to be cloned for each guest. */ + if (streq_ptr(ovmf_config->mode, "combined")) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Firmware descriptor '%s' declares flash mode 'combined', which is not supported.", + arg_firmware ?: ovmf_config->path); + + if (arg_confidential_computing != COCO_NO) { + const char *coco_feature = coco_firmware_feature_to_string(arg_confidential_computing); + + if (!ovmf_config_has_feature(ovmf_config, coco_feature)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), + "Firmware descriptor '%s' does not declare the '%s' feature, but " + "--coco=%s requires firmware built specifically for it.", + arg_firmware ?: ovmf_config->path, coco_feature, + confidential_computing_to_string(arg_confidential_computing)); + if (!ovmf_config_is_stateless(ovmf_config)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), + "Firmware descriptor '%s' does not describe stateless firmware, " + "but --coco=%s requires stateless firmware.", + arg_firmware ?: ovmf_config->path, + confidential_computing_to_string(arg_confidential_computing)); + if (!streq(ovmf_config_format(ovmf_config), "raw")) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), + "Firmware image '%s' is in %s format, " + "but --coco=%s requires a raw image.", + ovmf_config->path, ovmf_config_format(ovmf_config), + confidential_computing_to_string(arg_confidential_computing)); + } + + bool sb = ovmf_config_has_feature(ovmf_config, "secure-boot"); + if (set_contains(arg_firmware_features_include, "secure-boot") && !sb) return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Secure Boot requested, but selected OVMF firmware doesn't support it."); - log_debug("Using OVMF firmware %s Secure Boot support.", ovmf_config->supports_sb ? "with" : "without"); + log_debug("Using OVMF firmware %s Secure Boot support.", sb ? "with" : "without"); } _cleanup_(machine_bind_user_context_freep) MachineBindUserContext *bind_user_context = NULL; @@ -2742,7 +2773,8 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { } if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM && arg_confidential_computing == COCO_NO) { - r = qemu_config_key(config_file, "smm", on_off(ovmf_config->supports_sb)); + bool sb = ovmf_config_has_feature(ovmf_config, "secure-boot"); + r = qemu_config_key(config_file, "smm", on_off(sb)); if (r < 0) return r; } From 6f2f5b96f116f4bea31b86f85288c4cb83429d5a Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Fri, 3 Jul 2026 15:52:10 +0200 Subject: [PATCH 072/106] vmspawn: allow opt-in Secure Boot firmware for coco CoCo firmware is stateless, so Secure Boot keys cannot be enrolled at runtime: it only enforces Secure Boot with keys baked in at build time, refusing unsigned images from the first boot. The default exclusion of the enrolled-keys firmware feature hence keeps unsigned images bootable, but it also makes firmware discovery fail on distros that only ship SNP/TDX firmware with pre-enrolled keys (e.g. Fedora's TDVF). Drop the rejection of --secure-boot=yes with --coco= and instead treat it as an opt-in to such firmware, by lifting the enrolled-keys exclusion. While at it, reject --efi-nvram-template= and an explicit --efi-nvram-state= path with --coco=, which were silently ignored, as stateless firmware has no NVRAM to instantiate or persist. Signed-off-by: Paul Meyer --- man/systemd-vmspawn.xml | 19 +++++++++++++++---- src/vmspawn/vmspawn.c | 32 +++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 4ef1b18f57628..2475a0464f9ba 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -269,7 +269,8 @@ Takes an absolute path, or a relative path beginning with ./. Specifies the path to an EFI NVRAM template file to copy and use as the initial EFI variable NVRAM state. If not specified, the default NVRAM template from the firmware - definition is copied and used. + definition is copied and used. Cannot be used together with , as + confidential computing firmware is stateless. @@ -283,7 +284,8 @@ automatically derived from the VM image path or directory path, with the .efinvramstate suffix appended. If set to the special string off the EFI variable NVRAM state is only maintained transiently and flushed out - when the VM shuts down. Defaults to auto. + when the VM shuts down. Defaults to auto. An explicit path cannot be used + together with , as confidential computing firmware is stateless. If is specified, auto behaves like off. @@ -300,6 +302,13 @@ . Setting this to auto removes secure-boot from both the included and excluded feature lists. + With , setting this to yes additionally + requires the enrolled-keys firmware feature, overriding its default + exclusion: confidential computing firmware is stateless, so keys cannot be enrolled at runtime + and Secure Boot is only operative with keys baked into the image at build time, which in turn + makes the firmware refuse to boot unsigned images from the very first boot. The default + exclusion of enrolled-keys hence keeps unsigned images bootable. + @@ -353,7 +362,8 @@ SNP-capable hardware and firmware. A suitable SNP-built OVMF firmware is picked automatically from the installed QEMU firmware descriptors, by requiring the amd-sev-snp firmware feature; use with a path to a firmware descriptor file to - select a specific one. Secure Boot is unavailable. Direct kernel boot via + select a specific one. Secure Boot is effectively off by default (see + ). Direct kernel boot via is required so that the kernel, initrd and command line are hashed into the launch measurement (kernel-hashes=on); booting the kernel off the disk image via the firmware would leave it outside the measurement. Credentials passed via @@ -376,7 +386,8 @@ with a path to a firmware descriptor file to select a specific one. The CPU model is fixed to host. Firmware is measured into MRTD when the TD is built. Secure Boot cannot be enrolled at runtime (there is no writable NVRAM); its state - is fixed by the selected TDVF image and is part of the measured firmware. + is fixed by the selected TDVF image and is part of the measured firmware (see + ). When booting a UKI, the whole UKI PE is measured into RTMR 1, and the loaded sections are measured individually by systemd-stub into RTMR 2. For direct linux boot, firmware measures the kernel PE into RTMR 1, and the Linux EFI stub measures initrd and diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 2121fd29d0b04..32fd6908c99a1 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2574,6 +2574,14 @@ static int discover_ovmf_config(OvmfConfig **ret, sd_json_variant **ret_firmware r = set_put_strdup(&arg_firmware_features_include, coco_feature); if (r < 0) return log_oom(); + + /* CoCo firmware is stateless, so Secure Boot keys cannot be enrolled at runtime + * but must have baked-in, enrolled keys, which we avoid in other cases. */ + if (set_contains(arg_firmware_features_include, "secure-boot")) { + r = set_put_strdup(&arg_firmware_features_include, "enrolled-keys"); + if (r < 0) + return log_oom(); + } } FindOvmfConfigFlags flags = @@ -2584,7 +2592,8 @@ static int discover_ovmf_config(OvmfConfig **ret, sd_json_variant **ret_firmware if (r == -ENOENT && coco_feature) return log_error_errno(r, "No suitable firmware descriptor found for --coco=%s " "(requires stateless firmware in raw format with the '%s' firmware feature). " - "Install a suitable firmware or select a firmware descriptor with --firmware=.", + "Install a suitable firmware, select a firmware descriptor with --firmware=, or " + "opt in with --secure-boot=yes if the installed firmware enforces Secure Boot.", confidential_computing_to_string(arg_confidential_computing), coco_feature); if (r < 0) return log_error_errno(r, "Failed to find OVMF config: %m"); @@ -4244,6 +4253,19 @@ static int verify_arguments(void) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--grow-image is not supported for qcow2 images, use 'qemu-img resize FILE SIZE'."); + if (arg_confidential_computing != COCO_NO) { + /* Confidential computing firmware is stateless, there is no NVRAM to instantiate from a + * template or to persist. */ + if (arg_efi_nvram_template) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--efi-nvram-template= cannot be used with --coco=, " + "confidential computing firmware is stateless."); + if (arg_efi_nvram_state_mode == STATE_PATH) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "An explicit --efi-nvram-state= path cannot be used with --coco=, " + "confidential computing firmware is stateless. Use 'off' or 'auto'."); + } + if (arg_confidential_computing == COCO_AMD_SEV_SNP) { if (native_architecture() != ARCHITECTURE_X86_64) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), @@ -4255,9 +4277,6 @@ static int verify_arguments(void) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--coco=sev-snp can't be used with %s firmware", firmware_to_string(arg_firmware_type)); - if (set_contains(arg_firmware_features_include, "secure-boot")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--coco=sev-snp cannot be combined with --secure-boot=yes."); if (arg_tpm > 0) log_warning("TPM can't be trusted by the confidential computing guest"); /* kernel-hashes=on only covers what QEMU itself loads via -kernel/-initrd/-append. @@ -4281,11 +4300,6 @@ static int verify_arguments(void) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--coco=tdx can't be used with %s firmware", firmware_to_string(arg_firmware_type)); - /* Secure Boot state is baked into the supplied TDVF image and can't be enrolled at - * runtime (no writable NVRAM), so --secure-boot=yes would silently have no effect. */ - if (set_contains(arg_firmware_features_include, "secure-boot")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--coco=tdx cannot be combined with --secure-boot=yes."); if (arg_tpm > 0) log_warning("TPM can't be trusted by the confidential computing guest"); } From 2f72db10d9a0cdf3c1b22044df44c2e9fd61ff1b Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 17:55:37 +0100 Subject: [PATCH 073/106] sd-device: bound the tag filter BPF program size The tag-match loop emits 6 instructions per tag into a fixed 512-entry stack array without a bounds check (unlike the subsystem loop below), so enough tags overflow the stack. Refuse with -E2BIG instead. Follow-up for b1c097af8df58a94cba031a347061b7cb9b62d9b --- src/libsystemd/sd-device/device-monitor.c | 77 ++++++++++++------- .../sd-device/test-sd-device-monitor.c | 16 ++++ 2 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/libsystemd/sd-device/device-monitor.c b/src/libsystemd/sd-device/device-monitor.c index 962329a865ebd..7d9433920ec35 100644 --- a/src/libsystemd/sd-device/device-monitor.c +++ b/src/libsystemd/sd-device/device-monitor.c @@ -781,34 +781,57 @@ int device_monitor_send( return count; } -static void bpf_stmt(struct sock_filter *ins, unsigned *i, - unsigned short code, unsigned data) { +static int bpf_stmt_impl(struct sock_filter *ins, size_t *i, size_t n_ins, + unsigned short code, unsigned data) { + assert(ins); assert(i); + if (*i >= n_ins) + return -E2BIG; + ins[(*i)++] = (struct sock_filter) { .code = code, .k = data, }; + return 0; } -static void bpf_jmp(struct sock_filter *ins, unsigned *i, - unsigned short code, unsigned data, - unsigned short jt, unsigned short jf) { +#define bpf_stmt(ins, i, code, data) \ + bpf_stmt_impl((ins), (i), ELEMENTSOF(ins), (code), (data)) + +static int bpf_jmp_impl(struct sock_filter *ins, size_t *i, size_t n_ins, + unsigned short code, unsigned data, + unsigned jt, unsigned jf) { + assert(ins); assert(i); + if (*i >= n_ins) + return -E2BIG; + + /* The jump offsets are stored in single bytes (struct sock_filter.jt/.jf are __u8). A larger + * offset would be silently truncated and make the filter branch to the wrong instruction, i.e. + * drop events that should match. */ + if (jt > UINT8_MAX || jf > UINT8_MAX) + return -E2BIG; + ins[(*i)++] = (struct sock_filter) { .code = code, .jt = jt, .jf = jf, .k = data, }; + return 0; } +#define bpf_jmp(ins, i, code, data, jt, jf) \ + bpf_jmp_impl((ins), (i), ELEMENTSOF(ins), (code), (data), (jt), (jf)) + _public_ int sd_device_monitor_filter_update(sd_device_monitor *m) { struct sock_filter ins[512] = {}; struct sock_fprog filter; const char *subsystem, *devtype, *tag; - unsigned i = 0; + size_t i = 0; + int r; assert_return(m, -EINVAL); @@ -823,11 +846,11 @@ _public_ int sd_device_monitor_filter_update(sd_device_monitor *m) { } /* load magic in A */ - bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, magic)); + r = bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, magic)); /* jump if magic matches */ - bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, UDEV_MONITOR_MAGIC, 1, 0); + RET_GATHER(r, bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, UDEV_MONITOR_MAGIC, 1, 0)); /* wrong magic, pass packet */ - bpf_stmt(ins, &i, BPF_RET|BPF_K, 0xffffffff); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_RET|BPF_K, 0xffffffff)); if (!set_isempty(m->tag_filter)) { int tag_matches = set_size(m->tag_filter); @@ -839,23 +862,23 @@ _public_ int sd_device_monitor_filter_update(sd_device_monitor *m) { uint32_t tag_bloom_lo = tag_bloom_bits & 0xffffffff; /* load device bloom bits in A */ - bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, filter_tag_bloom_hi)); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, filter_tag_bloom_hi))); /* clear bits (tag bits & bloom bits) */ - bpf_stmt(ins, &i, BPF_ALU|BPF_AND|BPF_K, tag_bloom_hi); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_ALU|BPF_AND|BPF_K, tag_bloom_hi)); /* jump to next tag if it does not match */ - bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, tag_bloom_hi, 0, 3); + RET_GATHER(r, bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, tag_bloom_hi, 0, 3)); /* load device bloom bits in A */ - bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, filter_tag_bloom_lo)); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, filter_tag_bloom_lo))); /* clear bits (tag bits & bloom bits) */ - bpf_stmt(ins, &i, BPF_ALU|BPF_AND|BPF_K, tag_bloom_lo); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_ALU|BPF_AND|BPF_K, tag_bloom_lo)); /* jump behind end of tag match block if tag matches */ tag_matches--; - bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, tag_bloom_lo, 1 + (tag_matches * 6), 0); + RET_GATHER(r, bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, tag_bloom_lo, 1 + (tag_matches * 6), 0)); } /* nothing matched, drop packet */ - bpf_stmt(ins, &i, BPF_RET|BPF_K, 0); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_RET|BPF_K, 0)); } /* add all subsystem matches */ @@ -864,33 +887,33 @@ _public_ int sd_device_monitor_filter_update(sd_device_monitor *m) { uint32_t hash = string_hash32(subsystem); /* load device subsystem value in A */ - bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, filter_subsystem_hash)); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, filter_subsystem_hash))); if (!devtype) { /* jump if subsystem does not match */ - bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, hash, 0, 1); + RET_GATHER(r, bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, hash, 0, 1)); } else { /* jump if subsystem does not match */ - bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, hash, 0, 3); + RET_GATHER(r, bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, hash, 0, 3)); /* load device devtype value in A */ - bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, filter_devtype_hash)); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_LD|BPF_W|BPF_ABS, offsetof(monitor_netlink_header, filter_devtype_hash))); /* jump if value does not match */ hash = string_hash32(devtype); - bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, hash, 0, 1); + RET_GATHER(r, bpf_jmp(ins, &i, BPF_JMP|BPF_JEQ|BPF_K, hash, 0, 1)); } /* matched, pass packet */ - bpf_stmt(ins, &i, BPF_RET|BPF_K, 0xffffffff); - - if (i+1 >= ELEMENTSOF(ins)) - return -E2BIG; + RET_GATHER(r, bpf_stmt(ins, &i, BPF_RET|BPF_K, 0xffffffff)); } /* nothing matched, drop packet */ - bpf_stmt(ins, &i, BPF_RET|BPF_K, 0); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_RET|BPF_K, 0)); } /* matched, pass packet */ - bpf_stmt(ins, &i, BPF_RET|BPF_K, 0xffffffff); + RET_GATHER(r, bpf_stmt(ins, &i, BPF_RET|BPF_K, 0xffffffff)); + + if (r < 0) + return r; /* install filter */ filter = (struct sock_fprog) { diff --git a/src/libsystemd/sd-device/test-sd-device-monitor.c b/src/libsystemd/sd-device/test-sd-device-monitor.c index 8d88f3eb6727a..523cc7c6cd192 100644 --- a/src/libsystemd/sd-device/test-sd-device-monitor.c +++ b/src/libsystemd/sd-device/test-sd-device-monitor.c @@ -13,6 +13,7 @@ #include "path-util.h" #include "socket-util.h" #include "stat-util.h" +#include "stdio-util.h" #include "string-util.h" #include "tests.h" #include "time-util.h" @@ -282,6 +283,21 @@ TEST(sd_device_monitor_filter_add_match_tag) { ASSERT_EQ(sd_event_loop(sd_device_monitor_get_event(monitor_client)), 100); } +TEST(sd_device_monitor_filter_update_bounds) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *m = NULL; + + ASSERT_OK(device_monitor_new_full(&m, MONITOR_GROUP_NONE, -EBADF)); + + /* Each tag emits 6 BPF instructions into a fixed 512-entry array, too many must be refused */ + for (unsigned u = 0; u < 200; u++) { + char t[32]; + xsprintf(t, "tag%u", u); + ASSERT_OK(sd_device_monitor_filter_add_match_tag(m, t)); + } + + ASSERT_ERROR(sd_device_monitor_filter_update(m), E2BIG); +} + TEST(sd_device_monitor_filter_add_match_sysattr) { _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor_server = NULL, *monitor_client = NULL; _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; From 87e57c52c16030e73ea323578034cd5b67e60da2 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 18:02:10 +0100 Subject: [PATCH 074/106] hashmap: honor the value destructor in set_ensure_consume() Sets store their element in the key slot but use a value destructor (DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR). On the reject/duplicate path set_ensure_consume() only checked free_key and otherwise called free(). Follow-up for fcc1d0315d335ba31d85d6023c79d6404c62e167 --- src/basic/hashmap.c | 3 +++ src/test/test-set.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/basic/hashmap.c b/src/basic/hashmap.c index a2868e24557bd..89517b99f7241 100644 --- a/src/basic/hashmap.c +++ b/src/basic/hashmap.c @@ -1330,6 +1330,9 @@ int set_ensure_consume(Set **s, const struct hash_ops *hash_ops, void *key) { if (r <= 0) { if (hash_ops && hash_ops->free_key) hash_ops->free_key(key); + else if (hash_ops && hash_ops->free_value) + /* Sets store their element in the key slot but may carry a value destructor. */ + hash_ops->free_value(key); else free(key); } diff --git a/src/test/test-set.c b/src/test/test-set.c index 4c1872d636d3e..652b6837beb73 100644 --- a/src/test/test-set.c +++ b/src/test/test-set.c @@ -2,8 +2,10 @@ #include +#include "alloc-util.h" #include "random-util.h" #include "set.h" +#include "siphash24.h" #include "strv.h" #include "tests.h" @@ -51,6 +53,46 @@ TEST(set_free_with_hash_ops) { assert_se(items[3].seen == 0); } +static unsigned set_ensure_consume_freed = 0; + +typedef struct ValItem { + int key; +} ValItem; + +static void val_item_free(ValItem *i) { + set_ensure_consume_freed++; + free(i); +} + +static void val_item_hash_func(const ValItem *i, struct siphash *state) { + siphash24_compress_typesafe(i->key, state); +} + +static int val_item_compare_func(const ValItem *a, const ValItem *b) { + return CMP(a->key, b->key); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + val_item_hash_ops, ValItem, val_item_hash_func, val_item_compare_func, + ValItem, val_item_free); + +TEST(set_ensure_consume_value_destructor) { + _cleanup_set_free_ Set *s = NULL; + ValItem *a, *b; + + ASSERT_NOT_NULL(a = new0(ValItem, 1)); + a->key = 1; + ASSERT_OK_POSITIVE(set_ensure_consume(&s, &val_item_hash_ops, a)); + + ASSERT_NOT_NULL(b = new0(ValItem, 1)); + b->key = 1; /* same key, different pointer -> duplicate */ + + set_ensure_consume_freed = 0; + /* The rejected duplicate must be released via the value destructor, not plain free(). */ + ASSERT_OK_ZERO(set_ensure_consume(&s, &val_item_hash_ops, b)); + ASSERT_EQ(set_ensure_consume_freed, 1u); +} + TEST(set_put) { _cleanup_set_free_ Set *m = NULL; From 7003a9bf098869e5622fe5d863199910afcdda6d Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 18:26:57 +0100 Subject: [PATCH 075/106] sd-device: avoid 32-bit overflow in the monitor properties bounds check "properties_off + 32 > n" is evaluated in 32-bit arithmetic, so a properties_off near UINT32_MAX wraps but passes the check. Follow-up for efbd4b3ca84c0426b6ff98d6352f82f3b7c090b2 --- .../sd-device/device-monitor-private.h | 21 +++++++++++++++ src/libsystemd/sd-device/device-monitor.c | 27 +++---------------- .../sd-device/test-sd-device-monitor.c | 26 ++++++++++++++++++ 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/libsystemd/sd-device/device-monitor-private.h b/src/libsystemd/sd-device/device-monitor-private.h index 4582108df6163..dc1af6c19ca39 100644 --- a/src/libsystemd/sd-device/device-monitor-private.h +++ b/src/libsystemd/sd-device/device-monitor-private.h @@ -15,3 +15,24 @@ int device_monitor_new_full(sd_device_monitor **ret, MonitorNetlinkGroup group, int device_monitor_get_address(sd_device_monitor *m, union sockaddr_union *ret); int device_monitor_allow_unicast_sender(sd_device_monitor *m, sd_device_monitor *sender); int device_monitor_send(sd_device_monitor *m, const union sockaddr_union *destination, sd_device *device); + +#define UDEV_MONITOR_MAGIC 0xfeedcafe + +typedef struct monitor_netlink_header { + /* "libudev" prefix to distinguish libudev and kernel messages */ + char prefix[8]; + /* Magic to protect against daemon <-> Library message format mismatch + * Used in the kernel from socket filter rules; needs to be stored in network order */ + unsigned magic; + /* Total length of header structure known to the sender */ + unsigned header_size; + /* Properties string buffer */ + unsigned properties_off; + unsigned properties_len; + /* Hashes of primary device properties strings, to let libudev subscribers + * use in-kernel socket filters; values need to be stored in network order */ + unsigned filter_subsystem_hash; + unsigned filter_devtype_hash; + unsigned filter_tag_bloom_hi; + unsigned filter_tag_bloom_lo; +} monitor_netlink_header; diff --git a/src/libsystemd/sd-device/device-monitor.c b/src/libsystemd/sd-device/device-monitor.c index 7d9433920ec35..9799d9b4daca7 100644 --- a/src/libsystemd/sd-device/device-monitor.c +++ b/src/libsystemd/sd-device/device-monitor.c @@ -67,27 +67,6 @@ struct sd_device_monitor { void *userdata; }; -#define UDEV_MONITOR_MAGIC 0xfeedcafe - -typedef struct monitor_netlink_header { - /* "libudev" prefix to distinguish libudev and kernel messages */ - char prefix[8]; - /* Magic to protect against daemon <-> Library message format mismatch - * Used in the kernel from socket filter rules; needs to be stored in network order */ - unsigned magic; - /* Total length of header structure known to the sender */ - unsigned header_size; - /* Properties string buffer */ - unsigned properties_off; - unsigned properties_len; - /* Hashes of primary device properties strings, to let libudev subscribers - * use in-kernel socket filters; values need to be stored in network order */ - unsigned filter_subsystem_hash; - unsigned filter_devtype_hash; - unsigned filter_tag_bloom_hi; - unsigned filter_tag_bloom_lo; -} monitor_netlink_header; - static int monitor_set_nl_address(sd_device_monitor *m) { union sockaddr_union snl; socklen_t addrlen; @@ -642,10 +621,10 @@ _public_ int sd_device_monitor_receive(sd_device_monitor *m, sd_device **ret) { "Invalid message signature (%x != %x).", message.nlh->magic, htobe32(UDEV_MONITOR_MAGIC)); - if (message.nlh->properties_off + 32 > (size_t) n) + if (message.nlh->properties_off > LESS_BY((size_t) n, 32u)) return log_monitor_errno(m, SYNTHETIC_ERRNO(EAGAIN), - "Invalid offset for properties (%u > %zi).", - message.nlh->properties_off + 32, n); + "Invalid properties offset (%u) for message of length %zi.", + message.nlh->properties_off, n); offset = message.nlh->properties_off; diff --git a/src/libsystemd/sd-device/test-sd-device-monitor.c b/src/libsystemd/sd-device/test-sd-device-monitor.c index 523cc7c6cd192..c4034d27c7046 100644 --- a/src/libsystemd/sd-device/test-sd-device-monitor.c +++ b/src/libsystemd/sd-device/test-sd-device-monitor.c @@ -9,6 +9,7 @@ #include "device-private.h" #include "device-util.h" #include "io-util.h" +#include "iovec-util.h" #include "mountpoint-util.h" #include "path-util.h" #include "socket-util.h" @@ -424,6 +425,31 @@ TEST(sd_device_monitor_receive) { ASSERT_STREQ(s, syspath); } +TEST(sd_device_monitor_receive_bad_properties_off) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor_server = NULL, *monitor_client = NULL; + union sockaddr_union sa; + + prepare_monitor(&monitor_server, &monitor_client, &sa); + + monitor_netlink_header nlh = { + .prefix = "libudev", + .magic = htobe32(UDEV_MONITOR_MAGIC), + .header_size = sizeof nlh, + .properties_off = UINT32_MAX - 10, + }; + struct iovec iov = IOVEC_MAKE(&nlh, sizeof nlh); + struct msghdr smsg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_name = &sa, + .msg_namelen = sizeof(struct sockaddr_nl), + }; + ASSERT_OK_ERRNO(sendmsg(sd_device_monitor_get_fd(monitor_server), &smsg, 0)); + + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + ASSERT_ERROR(sd_device_monitor_receive(monitor_client, &dev), EAGAIN); +} + static int intro(void) { if (getuid() != 0) return log_tests_skipped("not root"); From a8d8009b7a0c2ec4f5d3097a0ae6fdb29fb0001c Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 18:37:24 +0100 Subject: [PATCH 076/106] udev: track remaining buffer size across $links devlinks FORMAT_SUBST_LINKS passes the same 'l' (remaining size) to strpcpy for each devlink and discards the returned remaining value, so the cursor advances while the bound stays at the full size, overflowing dest. Capture the return value. Follow-up for f6caab8995a27244db185f075e751a119e4bdedc --- src/udev/test-udev-format.c | 24 ++++++++++++++++++++++++ src/udev/udev-format.c | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/udev/test-udev-format.c b/src/udev/test-udev-format.c index 1acd8db72bf9b..b71e1e182a1ba 100644 --- a/src/udev/test-udev-format.c +++ b/src/udev/test-udev-format.c @@ -1,9 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-device.h" + +#include "device-private.h" #include "log.h" #include "mountpoint-util.h" #include "string-util.h" #include "tests.h" +#include "udev-event.h" #include "udev-format.h" static void test_udev_resolve_subsys_kernel_one(const char *str, bool read_value, int retval, const char *expected) { @@ -36,6 +40,26 @@ TEST(udev_resolve_subsys_kernel) { test_udev_resolve_subsys_kernel_one("[net/lo]/address", true, 0, "00:00:00:00:00:00"); } +TEST(udev_event_apply_format_links) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + _cleanup_(udev_event_unrefp) UdevEvent *event = NULL; + char dest[64]; + bool truncated = false; + + ASSERT_OK(sd_device_new_from_syspath(&dev, "/sys/class/net/lo")); + + for (unsigned u = 0; u < 32; u++) { + _cleanup_free_ char *l = NULL; + ASSERT_OK(asprintf(&l, "/dev/link-that-is-long-%u", u)); + ASSERT_OK(device_add_devlink(dev, l)); + } + + ASSERT_NOT_NULL((event = udev_event_new(dev, NULL, EVENT_TEST_SPAWN))); + + udev_event_apply_format(event, "$links", dest, sizeof dest, false, &truncated); + ASSERT_TRUE(truncated); +} + static int intro(void) { if (path_is_mount_point("/sys") <= 0) return log_tests_skipped("/sys is not mounted"); diff --git a/src/udev/udev-format.c b/src/udev/udev-format.c index d670ee129fdd9..c37ea0406f9b9 100644 --- a/src/udev/udev-format.c +++ b/src/udev/udev-format.c @@ -334,9 +334,9 @@ static ssize_t udev_event_subst_format( case FORMAT_SUBST_LINKS: FOREACH_DEVICE_DEVLINK(dev, link) { if (s == dest) - strpcpy_full(&s, l, link + STRLEN("/dev/"), &truncated); + l = strpcpy_full(&s, l, link + STRLEN("/dev/"), &truncated); else - strpcpyl_full(&s, l, &truncated, " ", link + STRLEN("/dev/"), NULL); + l = strpcpyl_full(&s, l, &truncated, " ", link + STRLEN("/dev/"), NULL); if (truncated) break; } From 9cae6d9c565e820d9a5bf0b35c8b73b6004553bb Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 18:47:27 +0100 Subject: [PATCH 077/106] network: initialize dot_servers before CLEANUP_ARRAY If dns_resolvers_to_dot_addrs() fails (e.g. OOM) it returns without assigning *ret_addrs, so the CLEANUP_ARRAY() cleanup would free an uninitialized dot_servers pointer on the error return. Follow-up for 3fd6708cde0fa4283e2335aca288a40a49406f3c --- src/network/networkd-state-file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network/networkd-state-file.c b/src/network/networkd-state-file.c index 3c505f39e6c95..f077ed5f4b946 100644 --- a/src/network/networkd-state-file.c +++ b/src/network/networkd-state-file.c @@ -134,7 +134,7 @@ static int link_put_dns(Link *link, OrderedSet **s) { r = sd_dhcp_lease_get_dnr(link->dhcp_lease, &resolvers); if (r >= 0) { - struct in_addr_full **dot_servers; + struct in_addr_full **dot_servers = NULL; size_t n = 0; CLEANUP_ARRAY(dot_servers, n, in_addr_full_free_array); @@ -163,7 +163,7 @@ static int link_put_dns(Link *link, OrderedSet **s) { r = sd_dhcp6_lease_get_dnr(link->dhcp6_lease, &resolvers); if (r >= 0) { - struct in_addr_full **dot_servers; + struct in_addr_full **dot_servers = NULL; size_t n = 0; CLEANUP_ARRAY(dot_servers, n, in_addr_full_free_array); From 25c016f472b775cd6d8bcf7fd29506663cbf81de Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 18:58:11 +0100 Subject: [PATCH 078/106] sd-netlink: disconnect the slot on async/match error paths netlink_slot_allocate() links the slot into nl->slots and takes an nl reference, but sd_netlink_call_async() and netlink_add_match_internal() hold it in a _cleanup_free_, so an error after allocation frees the slot with a bare free() Follow-up for ee38400bbaf04ff093c5450a554d5cc70face217 --- src/libsystemd/sd-netlink/sd-netlink.c | 31 +++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/libsystemd/sd-netlink/sd-netlink.c b/src/libsystemd/sd-netlink/sd-netlink.c index 65d731a20b139..8c2790e94b49e 100644 --- a/src/libsystemd/sd-netlink/sd-netlink.c +++ b/src/libsystemd/sd-netlink/sd-netlink.c @@ -548,7 +548,7 @@ int sd_netlink_call_async( uint64_t usec, const char *description) { - _cleanup_free_ sd_netlink_slot *slot = NULL; + _cleanup_(sd_netlink_slot_unrefp) sd_netlink_slot *slot = NULL; int r, k; assert_return(nl, -EINVAL); @@ -859,34 +859,39 @@ int netlink_add_match_internal( void *userdata, const char *description) { - _cleanup_free_ sd_netlink_slot *slot = NULL; + _cleanup_(sd_netlink_slot_unrefp) sd_netlink_slot *slot = NULL; int r; assert(groups); assert(n_groups > 0); - for (size_t i = 0; i < n_groups; i++) { - r = socket_broadcast_group_ref(nl, groups[i]); - if (r < 0) - return r; - } - r = netlink_slot_allocate(nl, !ret_slot, NETLINK_MATCH_CALLBACK, sizeof(struct match_callback), userdata, description, &slot); if (r < 0) return r; - slot->match_callback.groups = newdup(uint32_t, groups, n_groups); - if (!slot->match_callback.groups) - return -ENOMEM; - - slot->match_callback.n_groups = n_groups; slot->match_callback.callback = callback; slot->match_callback.type = type; slot->match_callback.cmd = cmd; + slot->match_callback.groups = newdup(uint32_t, groups, n_groups); + if (!slot->match_callback.groups) + return -ENOMEM; + + /* Link the callback before taking the broadcast group references, so that on a partial failure the + * cleanup path (sd_netlink_slot_unref() -> netlink_slot_disconnect()) can unlink it again and drop + * the references we already took. n_groups is bumped as each reference is acquired, so the disconnect + * only releases what we actually got. */ LIST_PREPEND(match_callbacks, nl->match_callbacks, &slot->match_callback); + for (size_t i = 0; i < n_groups; i++) { + r = socket_broadcast_group_ref(nl, groups[i]); + if (r < 0) + return r; + + slot->match_callback.n_groups = i + 1; + } + /* Set this at last. Otherwise, some failures in above call the destroy callback but some do not. */ slot->destroy_callback = destroy_callback; From 058b0de7bfda390cf08682b0179cf8f07685c0c3 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 19:26:23 +0100 Subject: [PATCH 079/106] sd-bus: re-check match_callbacks_modified in the argNhas value loop The CAN_HASH branch of bus_match_run() that iterates the array values of an argNhas= match dispatches a callback per value but, unlike the leaf and manual-iteration branches, does not re-check bus->match_callbacks_modified afterwards. A callback that adds or removes a match (freeing the node) would then have the next hashmap_get(node->compare.children, ...) read freed memory. Add the same check the sibling branches already have. Follow-up for 198b158f4941b817f26f8eb0ff75809bf5436496 --- src/libsystemd/sd-bus/bus-match.c | 3 ++ src/libsystemd/sd-bus/test-bus-match.c | 59 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/libsystemd/sd-bus/bus-match.c b/src/libsystemd/sd-bus/bus-match.c index f2521d21b0426..c365c5f3d844b 100644 --- a/src/libsystemd/sd-bus/bus-match.c +++ b/src/libsystemd/sd-bus/bus-match.c @@ -391,6 +391,9 @@ int bus_match_run( r = bus_match_run(bus, found, m); if (r != 0) return r; + + if (bus && bus->match_callbacks_modified) + return 0; } } diff --git a/src/libsystemd/sd-bus/test-bus-match.c b/src/libsystemd/sd-bus/test-bus-match.c index 58ba71e21f6fe..31d7602c671df 100644 --- a/src/libsystemd/sd-bus/test-bus-match.c +++ b/src/libsystemd/sd-bus/test-bus-match.c @@ -67,6 +67,63 @@ static void test_match_scope(const char *match, BusMatchScope scope) { assert_se(bus_match_get_scope(components, n_components) == scope); } +static sd_bus *modify_bus; +static BusMatchNode *modify_root; +static sd_bus_slot *modify_slots; + +static int strv_uaf_modify_filter(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + /* Remove both argNhas matches from within the callback (as bus_slot_disconnect() would), which + * frees the argNhas node that bus_match_run() is still iterating over the array values of. */ + assert_se(bus_match_remove(modify_root, &modify_slots[0].match_callback) >= 0); + assert_se(bus_match_remove(modify_root, &modify_slots[1].match_callback) >= 0); + modify_bus->match_callbacks_modified = true; + return 0; +} + +static int strv_uaf_noop_filter(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + return 0; +} + +static void add_cb_match(BusMatchNode *root, sd_bus_slot *slot, const char *match, sd_bus_message_handler_t cb) { + BusMatchComponent *components = NULL; + size_t n_components = 0; + + CLEANUP_ARRAY(components, n_components, bus_match_parse_free); + + assert_se(bus_match_parse(match, &components, &n_components) >= 0); + slot->n_ref = 1; /* bus_match_run() refs the slot around each callback */ + slot->match_callback.callback = cb; + assert_se(bus_match_add(root, components, n_components, &slot->match_callback) >= 0); +} + +/* Two argNhas= matches under the same node; the first callback frees that node. bus_match_run() must + * not dereference it for the next array value. */ +static void test_match_run_strv_modified(sd_bus *bus) { + BusMatchNode root = { .type = BUS_MATCH_ROOT }; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + sd_bus_slot slots[2] = {}; + + modify_bus = bus; + modify_root = &root; + modify_slots = slots; + + add_cb_match(&root, &slots[0], "arg0has='pi'", strv_uaf_modify_filter); + add_cb_match(&root, &slots[1], "arg0has='pa'", strv_uaf_noop_filter); + + assert_se(sd_bus_message_new_signal(bus, &m, "/", "a.b", "c") >= 0); + assert_se(sd_bus_message_append(m, "as", 2, "pi", "pa") >= 0); + assert_se(sd_bus_message_seal(m, 1, 0) >= 0); + + /* Let the leaf gating pass for our synthetic message/matches. */ + m->read_counter = 1; + bus->iteration_counter = 1; + bus->match_callbacks_modified = false; + + assert_se(bus_match_run(bus, &root, m) == 0); + + bus_match_free(&root); +} + int main(int argc, char *argv[]) { BusMatchNode root = { .type = BUS_MATCH_ROOT, @@ -142,5 +199,7 @@ int main(int argc, char *argv[]) { test_match_scope("member='gurke',path='/org/freedesktop/DBus/Local'", BUS_MATCH_LOCAL); test_match_scope("arg2='piep',sender='org.freedesktop.DBus',member='waldo'", BUS_MATCH_DRIVER); + test_match_run_strv_modified(bus); + return 0; } From b7b13032092c93a0ee5457beeabf65808c8a1433 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 20:08:14 +0100 Subject: [PATCH 080/106] sd-bus: drop half-registered vtable members on failure add_object_vtable_internal() inserts BusVTableMember entries into the global vtable_methods/vtable_properties sets as it walks the vtable, but only sets node_vtable.node (which gates the disconnect-time cleanup) after the loop. A failure mid-loop thus left the already-inserted members behind, dangling once the slot's interface string and the node's path were freed, so a later registration reading those keys hit a use-after-free. Remove this slot's members on the fail path. The test asserts the failed registration leaves no member behind. Follow-up for 19befb2d5fc087f96e40ddc432b2cc9385666209 --- src/libsystemd/sd-bus/bus-objects.c | 31 ++++++++++++++++++++++++ src/libsystemd/sd-bus/test-bus-objects.c | 22 +++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/libsystemd/sd-bus/bus-objects.c b/src/libsystemd/sd-bus/bus-objects.c index 795cc68837655..aa371cceca8fe 100644 --- a/src/libsystemd/sd-bus/bus-objects.c +++ b/src/libsystemd/sd-bus/bus-objects.c @@ -2099,6 +2099,37 @@ static int add_object_vtable_internal( return 0; fail: + /* Remove members we already inserted into the global tables before failing (identified by + * parent), so they don't dangle once the slot and node are freed below. */ + if (s && s->node_vtable.interface) + for (v = bus_vtable_next(vtable, vtable); v->type != _SD_BUS_VTABLE_END; v = bus_vtable_next(vtable, v)) { + BusVTableMember key, *x; + Set *set; + + if (v->type == _SD_BUS_VTABLE_METHOD) { + set = bus->vtable_methods; + key.member = v->x.method.member; + } else if (IN_SET(v->type, _SD_BUS_VTABLE_PROPERTY, _SD_BUS_VTABLE_WRITABLE_PROPERTY)) { + set = bus->vtable_properties; + key.member = v->x.property.member; + } else + continue; + + /* The entry that failed validation may carry a NULL member, which was never + * inserted and would crash the hashmap lookup below. Skip it. */ + if (isempty(key.member)) + continue; + + key.path = n->path; + key.interface = s->node_vtable.interface; + + x = set_get(set, &key); + if (x && x->parent == &s->node_vtable) { + assert_se(set_remove(set, &key) == x); + free(x); + } + } + sd_bus_slot_unref(s); bus_node_gc(bus, n); diff --git a/src/libsystemd/sd-bus/test-bus-objects.c b/src/libsystemd/sd-bus/test-bus-objects.c index ac33086a6f374..5d8e772cb441e 100644 --- a/src/libsystemd/sd-bus/test-bus-objects.c +++ b/src/libsystemd/sd-bus/test-bus-objects.c @@ -7,6 +7,7 @@ #include "bus-internal.h" #include "bus-message.h" #include "log.h" +#include "set.h" #include "strv.h" #include "tests.h" @@ -566,6 +567,25 @@ static int client(void *p) { return 0; } +static const sd_bus_vtable partial_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("Foo", NULL, NULL, NULL, 0), + SD_BUS_METHOD("has space", NULL, NULL, NULL, 0), /* invalid member name, registration fails here */ + SD_BUS_VTABLE_END +}; + +static void test_vtable_partial_failure(void) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + + ASSERT_OK(sd_bus_new(&bus)); + + /* Registration fails at the invalid member, after "Foo" was already inserted into the global vtable + * member tables. Check that the fail path removes it again. */ + ASSERT_ERROR(sd_bus_add_object_vtable(bus, NULL, "/x", "org.test.iface", partial_vtable, NULL), EINVAL); + + ASSERT_TRUE(set_isempty(bus->vtable_methods)); +} + int main(int argc, char *argv[]) { _cleanup_(sd_event_unrefp) sd_event *e = NULL; _cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client = NULL; @@ -573,6 +593,8 @@ int main(int argc, char *argv[]) { test_setup_logging(LOG_DEBUG); + test_vtable_partial_failure(); + c.automatic_integer_property = 4711; ASSERT_NOT_NULL(c.automatic_string_property = strdup("dudeldu")); From 7eb7d46f9750d3baf9eaa9f3edb64d5c00fc25cf Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 20:55:53 +0100 Subject: [PATCH 081/106] udev: clear event back-pointer when freeing a worker worker_free() did not clear worker->event->worker, leaving an event that was still attached to a worker pointing at freed memory once the worker was gone. manager_free() frees the workers (hashmap_free()) before it walks manager->events to process them, and event_free() then writes event->worker->event = NULL into the already-freed worker. Clear the back-pointer in worker_free(), mirroring event_free() which already clears the worker's pointer. The link used to be torn down by the event_free(worker->event) call in worker_free(), which was dropped when events became reference counted. The Event and Worker definitions move to udev-manager.h (next to Manager) so the unit test can construct and free a bound worker/event pair. Follow-up for cb16d47f30cc0d848ca6868c39b844810520d713 --- src/udev/test-udev-manager.c | 19 +++++++++++ src/udev/udev-manager.c | 64 ++++-------------------------------- src/udev/udev-manager.h | 62 ++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 58 deletions(-) diff --git a/src/udev/test-udev-manager.c b/src/udev/test-udev-manager.c index d444b0bf00ca2..7de5a37289c65 100644 --- a/src/udev/test-udev-manager.c +++ b/src/udev/test-udev-manager.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "alloc-util.h" #include "tests.h" #include "udev-manager.h" @@ -16,4 +17,22 @@ TEST(devpath_conflict) { "/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0/nvme/nvme0/nvme0n1/nvme0n1p1")); } +TEST(worker_free_detaches_event) { + /* worker_free() must clear the back-pointer of the event it is processing, otherwise the event is + * left referencing the freed worker. This happens e.g. on manager_free(), which frees the workers + * before the events still referencing them. */ + + Worker *worker = ASSERT_PTR(new0(Worker, 1)); + worker->pidref = PIDREF_NULL; + + _cleanup_free_ Event *event = ASSERT_PTR(new0(Event, 1)); + + worker->event = event; + event->worker = worker; + + worker_free(worker); + + ASSERT_NULL(event->worker); +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/udev/udev-manager.c b/src/udev/udev-manager.c index df13df6b50680..fabbdbb88c9f9 100644 --- a/src/udev/udev-manager.c +++ b/src/udev/udev-manager.c @@ -50,63 +50,6 @@ #define EVENT_REQUEUE_INTERVAL_USEC (200 * USEC_PER_MSEC) #define EVENT_REQUEUE_TIMEOUT_USEC (3 * USEC_PER_MINUTE) -typedef enum EventState { - EVENT_UNDEF, - EVENT_QUEUED, - EVENT_RUNNING, - EVENT_LOCKED, - EVENT_PROCESSED, -} EventState; - -typedef struct Event { - /* All events that have not been processed (state != EVENT_PROCESSED) are referenced by the Manager. - * Additionally, an event may be referenced by events blocked by this event. See event_find_blocker(). */ - unsigned n_ref; - - Manager *manager; - Worker *worker; - EventState state; - - sd_device *dev; - - sd_device_action_t action; - uint64_t seqnum; - const char *id; - const char *devpath; - const char *devpath_old; - const char *devnode; - - /* Used when the device is locked by another program. */ - usec_t requeue_next_usec; - usec_t requeue_timeout_usec; - unsigned locked_event_prioq_index; - char *whole_disk; - LIST_FIELDS(Event, same_disk); - - /* The last blocker for this event. This event must not be processed before the blocker is processed. */ - Event *blocker; - - LIST_FIELDS(Event, event); -} Event; - -typedef enum WorkerState { - WORKER_UNDEF, - WORKER_RUNNING, - WORKER_IDLE, - WORKER_KILLED, -} WorkerState; - -typedef struct Worker { - Manager *manager; - PidRef pidref; - sd_event_source *child_event_source; - sd_event_source *timeout_warning_event_source; - sd_event_source *timeout_kill_event_source; - union sockaddr_union address; - WorkerState state; - Event *event; -} Worker; - static void event_unset_whole_disk(Event *event) { Manager *manager = ASSERT_PTR(ASSERT_PTR(event)->manager); @@ -171,13 +114,18 @@ static Event* event_enter_processed(Event *event) { DEFINE_TRIVIAL_CLEANUP_FUNC(Event*, event_enter_processed); -static Worker* worker_free(Worker *worker) { +Worker* worker_free(Worker *worker) { if (!worker) return NULL; if (worker->manager) hashmap_remove(worker->manager->workers, &worker->pidref); + /* If an event is still attached, clear its back-pointer so it doesn't dangle (mirrors event_free(), + * which clears the worker's pointer). */ + if (worker->event) + worker->event->worker = NULL; + sd_event_source_disable_unref(worker->child_event_source); sd_event_source_unref(worker->timeout_warning_event_source); sd_event_source_unref(worker->timeout_kill_event_source); diff --git a/src/udev/udev-manager.h b/src/udev/udev-manager.h index c1b98c52d1da3..4e18dd540475f 100644 --- a/src/udev/udev-manager.h +++ b/src/udev/udev-manager.h @@ -1,9 +1,12 @@ /* SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once +#include "sd-device.h" #include "sd-event.h" #include "list.h" +#include "pidref.h" +#include "socket-util.h" #include "udev-config.h" #include "udev-forward.h" @@ -76,10 +79,69 @@ typedef struct Manager { bool exit; } Manager; +typedef enum EventState { + EVENT_UNDEF, + EVENT_QUEUED, + EVENT_RUNNING, + EVENT_LOCKED, + EVENT_PROCESSED, +} EventState; + +typedef struct Event { + /* All events that have not been processed (state != EVENT_PROCESSED) are referenced by the Manager. + * Additionally, an event may be referenced by events blocked by this event. See event_find_blocker(). */ + unsigned n_ref; + + Manager *manager; + Worker *worker; + EventState state; + + sd_device *dev; + + sd_device_action_t action; + uint64_t seqnum; + const char *id; + const char *devpath; + const char *devpath_old; + const char *devnode; + + /* Used when the device is locked by another program. */ + usec_t requeue_next_usec; + usec_t requeue_timeout_usec; + unsigned locked_event_prioq_index; + char *whole_disk; + LIST_FIELDS(Event, same_disk); + + /* The last blocker for this event. This event must not be processed before the blocker is processed. */ + Event *blocker; + + LIST_FIELDS(Event, event); +} Event; + +typedef enum WorkerState { + WORKER_UNDEF, + WORKER_RUNNING, + WORKER_IDLE, + WORKER_KILLED, +} WorkerState; + +typedef struct Worker { + Manager *manager; + PidRef pidref; + sd_event_source *child_event_source; + sd_event_source *timeout_warning_event_source; + sd_event_source *timeout_kill_event_source; + union sockaddr_union address; + WorkerState state; + Event *event; +} Worker; + Manager* manager_new(void); Manager* manager_free(Manager *manager); DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); +Worker* worker_free(Worker *worker); + int manager_main(Manager *manager); void manager_reload(Manager *manager, bool force); void manager_revert(Manager *manager); From 134df27257c8ff8b63fdd36dc0778f2229c7229b Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 22:06:25 +0100 Subject: [PATCH 082/106] network: implement refcounting for SR-IOV objects link_request_sr_iov_vfs() queued each request with a raw SRIOV* as userdata and a NULL free_func, so the request did not own the object. The SRIOV is owned by link->network->sr_iov_by_section, which is freed by network_free() on reload while the RTM_SETLINK reply may still be in flight (the floating netlink slot keeps the request alive but does not keep the userdata alive). A later reply, or the 25s netlink timeout, then dispatches sr_iov_handler() which reads the freed SRIOV. Follow-up for cb8453cc51a9d49e094e746af2e074669a71cc4a --- src/network/networkd-sriov.c | 5 ++++- src/shared/netif-sriov.c | 43 ++++++++++++++++++++++++++---------- src/shared/netif-sriov.h | 4 ++++ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/network/networkd-sriov.c b/src/network/networkd-sriov.c index 466442597916f..d35e108a4a956 100644 --- a/src/network/networkd-sriov.c +++ b/src/network/networkd-sriov.c @@ -98,11 +98,12 @@ int link_request_sr_iov_vfs(Link *link) { if (!sr_iov_has_config(sr_iov, attr)) continue; + /* The request takes a reference on the configuration. */ r = link_queue_request_safe( link, _REQUEST_TYPE_SRIOV_BASE + attr, sr_iov, - NULL, + sr_iov_unref, sr_iov_hash_func, sr_iov_compare_func, sr_iov_process_request, @@ -113,6 +114,8 @@ int link_request_sr_iov_vfs(Link *link) { return log_link_warning_errno(link, r, "Failed to request to set up %s for SR-IOV virtual function %"PRIu32": %m", sr_iov_attribute_to_string(attr), sr_iov->vf); + if (r > 0) + sr_iov_ref(sr_iov); } } diff --git a/src/shared/netif-sriov.c b/src/shared/netif-sriov.c index f5df7fb392ac6..79fd86c4f67d7 100644 --- a/src/shared/netif-sriov.c +++ b/src/shared/netif-sriov.c @@ -16,24 +16,42 @@ #include "string-table.h" #include "string-util.h" +static SRIOV* sr_iov_detach_impl(SRIOV *sr_iov) { + assert(sr_iov); + + if (!sr_iov->sr_iov_by_section) + return NULL; + + assert(sr_iov->section); + ordered_hashmap_remove(sr_iov->sr_iov_by_section, sr_iov->section); + sr_iov->sr_iov_by_section = NULL; + + return sr_iov; +} + static SRIOV* sr_iov_free(SRIOV *sr_iov) { if (!sr_iov) return NULL; - if (sr_iov->sr_iov_by_section && sr_iov->section) - ordered_hashmap_remove(sr_iov->sr_iov_by_section, sr_iov->section); + sr_iov_detach_impl(sr_iov); config_section_free(sr_iov->section); return mfree(sr_iov); } -DEFINE_SECTION_CLEANUP_FUNCTIONS(SRIOV, sr_iov_free); +DEFINE_TRIVIAL_REF_UNREF_FUNC(SRIOV, sr_iov, sr_iov_free); +DEFINE_SECTION_CLEANUP_FUNCTIONS(SRIOV, sr_iov_unref); + +static void sr_iov_detach(SRIOV *sr_iov) { + assert(sr_iov); + sr_iov_unref(sr_iov_detach_impl(sr_iov)); +} DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( sr_iov_hash_ops_by_section, ConfigSection, config_section_hash_func, config_section_compare_func, - SRIOV, sr_iov_free); + SRIOV, sr_iov_detach); static int sr_iov_new(SRIOV **ret) { SRIOV *sr_iov; @@ -45,6 +63,7 @@ static int sr_iov_new(SRIOV **ret) { return -ENOMEM; *sr_iov = (SRIOV) { + .n_ref = 1, .vf = UINT32_MAX, .vlan_proto = ETH_P_8021Q, .vf_spoof_check_setting = -1, @@ -60,7 +79,7 @@ static int sr_iov_new(SRIOV **ret) { static int sr_iov_new_static(OrderedHashmap **sr_iov_by_section, const char *filename, unsigned section_line, SRIOV **ret) { _cleanup_(config_section_freep) ConfigSection *n = NULL; - _cleanup_(sr_iov_freep) SRIOV *sr_iov = NULL; + _cleanup_(sr_iov_unrefp) SRIOV *sr_iov = NULL; SRIOV *existing = NULL; int r; @@ -368,7 +387,7 @@ int sr_iov_drop_invalid_sections(uint32_t num_vfs, OrderedHashmap *sr_iov_by_sec SRIOV *dup; if (sr_iov_section_verify(num_vfs, sr_iov) < 0) { - sr_iov_free(sr_iov); + sr_iov_detach(sr_iov); continue; } @@ -378,7 +397,7 @@ int sr_iov_drop_invalid_sections(uint32_t num_vfs, OrderedHashmap *sr_iov_by_sec "dropping the [SR-IOV] section specified at line %u.", dup->section->filename, sr_iov->section->line, dup->section->line, dup->section->line); - sr_iov_free(dup); + sr_iov_detach(dup); } r = set_ensure_put(&set, &sr_iov_hash_ops, sr_iov); @@ -402,7 +421,7 @@ int config_parse_sr_iov_uint32( void *data, void *userdata) { - _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL; + _cleanup_(sr_iov_unref_or_set_invalidp) SRIOV *sr_iov = NULL; OrderedHashmap **sr_iov_by_section = ASSERT_PTR(data); uint32_t k; int r; @@ -469,7 +488,7 @@ int config_parse_sr_iov_vlan_proto( void *data, void *userdata) { - _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL; + _cleanup_(sr_iov_unref_or_set_invalidp) SRIOV *sr_iov = NULL; OrderedHashmap **sr_iov_by_section = ASSERT_PTR(data); int r; @@ -507,7 +526,7 @@ int config_parse_sr_iov_link_state( void *data, void *userdata) { - _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL; + _cleanup_(sr_iov_unref_or_set_invalidp) SRIOV *sr_iov = NULL; OrderedHashmap **sr_iov_by_section = ASSERT_PTR(data); int r; @@ -558,7 +577,7 @@ int config_parse_sr_iov_boolean( void *data, void *userdata) { - _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL; + _cleanup_(sr_iov_unref_or_set_invalidp) SRIOV *sr_iov = NULL; OrderedHashmap **sr_iov_by_section = ASSERT_PTR(data); int r; @@ -615,7 +634,7 @@ int config_parse_sr_iov_mac( void *data, void *userdata) { - _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL; + _cleanup_(sr_iov_unref_or_set_invalidp) SRIOV *sr_iov = NULL; OrderedHashmap **sr_iov_by_section = ASSERT_PTR(data); int r; diff --git a/src/shared/netif-sriov.h b/src/shared/netif-sriov.h index cd2befc634084..578a7ea579f12 100644 --- a/src/shared/netif-sriov.h +++ b/src/shared/netif-sriov.h @@ -27,6 +27,8 @@ typedef enum SRIOVLinkState { } SRIOVLinkState; typedef struct SRIOV { + unsigned n_ref; + ConfigSection *section; OrderedHashmap *sr_iov_by_section; @@ -43,6 +45,8 @@ typedef struct SRIOV { DECLARE_STRING_TABLE_LOOKUP_TO_STRING(sr_iov_attribute, SRIOVAttribute); +DECLARE_TRIVIAL_REF_UNREF_FUNC(SRIOV, sr_iov); + void sr_iov_hash_func(const SRIOV *sr_iov, struct siphash *state); int sr_iov_compare_func(const SRIOV *s1, const SRIOV *s2); bool sr_iov_has_config(SRIOV *sr_iov, SRIOVAttribute attr); From 46c15e88e0c2d78679670a60d3406e74de455036 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 2 Jul 2026 22:20:31 +0100 Subject: [PATCH 083/106] resolve: anchor the service browser from mDNS maintenance queries mdns_maintenance_query() takes a ref on the browser's varlink link but never installs itself as that link's userdata. dns_query_free() then unconditionally runs sd_varlink_set_userdata(varlink_request, NULL), so freeing any maintenance query wipes the browse query's registration on the shared sb->link slot, disabling the abort paths in vl_on_disconnect() and dns_service_browser_free(). The maintenance query also never takes a reference on the DnsServiceBrowser, so a client disconnect could free the browser while a maintenance query was still in flight, leaving the per-service schedule_event timer and the raw service->service_browser back-pointer dangling (use-after-free on the next timer tick or query completion). Take a service_browser_request reference instead, matching the browse query path. dns_query_free() already drops it. The browser now outlives its in-flight maintenance queries. Follow-up for 8458b7fb91ea5e5109b6f3c94f8a781a120c798b --- src/resolve/resolved-dns-browse-services.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolve/resolved-dns-browse-services.c b/src/resolve/resolved-dns-browse-services.c index 68dcbee349bb5..6a48089a594af 100644 --- a/src/resolve/resolved-dns-browse-services.c +++ b/src/resolve/resolved-dns-browse-services.c @@ -126,7 +126,7 @@ static int mdns_maintenance_query(sd_event_source *s, uint64_t usec, void *userd return log_error_errno(r, "Failed to create mDNS query for maintenance: %m"); q->complete = mdns_maintenance_query_complete; - q->varlink_request = sd_varlink_ref(service->service_browser->link); + q->service_browser_request = dns_service_browser_ref(service->service_browser); q->dnsservice_request = dnssd_discovered_service_ref(service); /* Schedule the next maintenance query based on the TTL */ From 6bfd3424e89816e6814413c164ad5127f1568849 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 2 Jul 2026 15:27:06 +0200 Subject: [PATCH 084/106] journalctl: add follow mode to the varlink method This commit implements `journalctl -f` like behavior for the varlink API of journalctl. It is used via: ``` $ varlinkctl call -E \ /run/systemd/io.systemd.JournalAccess \ io.systemd.JournalAccess.GetEntries '{"follow": true, "limit": 10}' ``` This gives the last 10 message and then it keeps the connection open and output each new log line that matches the set filters. The code is modeled after `journalctl -f`. It seems there is little to extract into shared code here so I left it for now. --- src/journal/journalctl-varlink-server.c | 188 ++++++++++++++++-- src/journal/journalctl-varlink-server.h | 1 + src/journal/journalctl.c | 5 + src/shared/varlink-io.systemd.JournalAccess.c | 2 + .../TEST-04-JOURNAL.journalctl-varlink.sh | 53 +++++ 5 files changed, 237 insertions(+), 12 deletions(-) diff --git a/src/journal/journalctl-varlink-server.c b/src/journal/journalctl-varlink-server.c index 85b4e225f5137..8c662211a9a68 100644 --- a/src/journal/journalctl-varlink-server.c +++ b/src/journal/journalctl-varlink-server.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-event.h" #include "sd-journal.h" #include "sd-varlink.h" @@ -22,6 +23,7 @@ typedef struct GetEntriesParameters { uid_t uid; int priority; uint64_t limit; + bool follow; } GetEntriesParameters; static void get_entries_parameters_done(GetEntriesParameters *p) { @@ -31,6 +33,148 @@ static void get_entries_parameters_done(GetEntriesParameters *p) { p->user_units = strv_free(p->user_units); } +typedef struct FollowState { + sd_journal *journal; + sd_varlink *link; + sd_event_source *io_event_source; +} FollowState; + +static FollowState* follow_state_free(FollowState *fs) { + if (!fs) + return NULL; + + sd_event_source_disable_unref(fs->io_event_source); + sd_journal_close(fs->journal); + sd_varlink_unref(fs->link); + + return mfree(fs); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(FollowState*, follow_state_free); + +void vl_on_disconnect(sd_varlink_server *server, sd_varlink *link, void *userdata) { + assert(server); + assert(link); + + follow_state_free(sd_varlink_set_userdata(link, NULL)); +} + +static int entry_to_json_and_send(sd_journal *j, sd_varlink *link, bool follow) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *entry = NULL; + int r; + + assert(j); + assert(link); + + r = journal_entry_to_json(j, OUTPUT_SHOW_ALL, /* output_fields= */ NULL, &entry); + if (r <= 0) + return r; /* 0: skip corrupted entry */ + + if (follow) + r = sd_varlink_notifybo(link, SD_JSON_BUILD_PAIR_VARIANT("entry", entry)); + else + r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_VARIANT("entry", entry)); + if (r < 0) + return r; + + return 1; +} + +static int follow_drain(FollowState *fs) { + int r; + + assert(fs); + + for (;;) { + r = sd_journal_next(fs->journal); + if (r < 0) + return r; + if (r == 0) + return 0; + + r = entry_to_json_and_send(fs->journal, fs->link, /* follow= */ true); + if (r < 0) + return r; + } +} + +static int follow_dispatch(FollowState *fs) { + int r; + + assert(fs); + + r = sd_journal_process(fs->journal); + if (r < 0) + goto fail; + + if (r != SD_JOURNAL_NOP) { + r = follow_drain(fs); + if (r < 0) + goto fail; + } + + return 0; + +fail: + log_warning_errno(r, "Failed to stream journal entries, completing call: %m"); + + _cleanup_(sd_varlink_unrefp) sd_varlink *link = sd_varlink_ref(fs->link); + + /* drop our state before replying, so that the disconnect callback won't free it a second time */ + follow_state_free(sd_varlink_set_userdata(link, NULL)); + + (void) sd_varlink_error_errno(link, r); + + return 0; +} + +static int follow_on_journal_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + FollowState *fs = ASSERT_PTR(userdata); + + return follow_dispatch(fs); +} + +static int follow_state_setup(sd_varlink *link, sd_journal *_j /* donated! */) { + _cleanup_(sd_journal_closep) sd_journal *j = TAKE_PTR(_j); /* take possession in all cases */ + _cleanup_(follow_state_freep) FollowState *fs = NULL; + int r; + + assert(link); + assert(j); + + sd_varlink_server *server = ASSERT_PTR(sd_varlink_get_server(link)); + sd_event *event = ASSERT_PTR(sd_varlink_server_get_event(server)); + + fs = new(FollowState, 1); + if (!fs) + return -ENOMEM; + + *fs = (FollowState) { + .journal = TAKE_PTR(j), + .link = sd_varlink_ref(link), + }; + + int fd = sd_journal_get_fd(fs->journal); + if (fd < 0) + return fd; + + int events = sd_journal_get_events(fs->journal); + if (events < 0) + return events; + + r = sd_event_add_io(event, &fs->io_event_source, fd, (uint32_t) events, follow_on_journal_io, fs); + if (r < 0) + return r; + + (void) sd_event_source_set_description(fs->io_event_source, "journal-follow-io"); + + /* stashed on the link for vl_on_disconnect(); the link ref taken above also tells the varlink + * dispatcher that the call is deliberately left pending */ + sd_varlink_set_userdata(link, TAKE_PTR(fs)); + + return 0; +} + int vl_method_get_entries(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { static const sd_json_dispatch_field dispatch_table[] = { @@ -40,6 +184,7 @@ int vl_method_get_entries(sd_varlink *link, sd_json_variant *parameters, sd_varl { "namespace", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(GetEntriesParameters, namespace), 0 }, { "priority", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_log_level, offsetof(GetEntriesParameters, priority), 0 }, { "limit", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(GetEntriesParameters, limit), 0 }, + { "follow", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(GetEntriesParameters, follow), 0 }, {} }; @@ -62,7 +207,7 @@ int vl_method_get_entries(sd_varlink *link, sd_json_variant *parameters, sd_varl /* systemd ships with sensible defaults for the system/user services and the socket permissions so we * do not need to do extra sd_varlink_get_peer_uid() or policykit checks here */ - r = sd_journal_open_namespace(&j, p.namespace, SD_JOURNAL_LOCAL_ONLY | SD_JOURNAL_ASSUME_IMMUTABLE); + r = sd_journal_open_namespace(&j, p.namespace, SD_JOURNAL_LOCAL_ONLY | (p.follow ? 0 : SD_JOURNAL_ASSUME_IMMUTABLE)); if (r < 0) return r; @@ -84,6 +229,13 @@ int vl_method_get_entries(sd_varlink *link, sd_json_variant *parameters, sd_varl return r; } + /* create the inotify watch before draining the backlog, so entries logged in between wake us up */ + if (p.follow) { + r = sd_journal_get_fd(j); + if (r < 0) + return r; + } + /* this simulates "journalctl -n $p.limit" */ r = sd_journal_seek_tail(j); if (r < 0) @@ -99,28 +251,40 @@ int vl_method_get_entries(sd_varlink *link, sd_json_variant *parameters, sd_varl if (r < 0) return r; - r = sd_varlink_set_sentinel(link, "io.systemd.JournalAccess.NoEntries"); - if (r < 0) - return r; + /* no sentinel in follow mode: an empty backlog is fine, the stream simply stays open */ + if (!p.follow) { + r = sd_varlink_set_sentinel(link, "io.systemd.JournalAccess.NoEntries"); + if (r < 0) + return r; + } + uint64_t n_sent = 0; for (uint64_t i = 0; i < n; i++) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *entry = NULL; - r = sd_journal_next(j); if (r < 0) return r; if (r == 0) break; - r = journal_entry_to_json(j, OUTPUT_SHOW_ALL, /* output_fields= */ NULL, &entry); + r = entry_to_json_and_send(j, link, p.follow); if (r < 0) return r; - if (r == 0) - continue; /* skip corrupted entry */ - r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_VARIANT("entry", entry)); - if (r < 0) - return r; + n_sent++; + } + + if (p.follow) { + if (n_sent == 0) { + /* The backlog matched nothing, hence the journal contains no matching entry at all + * and anything that matches from now on is new. Seek to head like journalctl's + * on_first_event() does, instead of to the current realtime, which would race + * against entries logged while we were setting up. */ + r = sd_journal_seek_head(j); + if (r < 0) + return r; + } + + return follow_state_setup(link, TAKE_PTR(j)); } return 0; diff --git a/src/journal/journalctl-varlink-server.h b/src/journal/journalctl-varlink-server.h index 45cf0e6918fc7..102f86de8a279 100644 --- a/src/journal/journalctl-varlink-server.h +++ b/src/journal/journalctl-varlink-server.h @@ -4,3 +4,4 @@ #include "sd-varlink.h" int vl_method_get_entries(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); +void vl_on_disconnect(sd_varlink_server *server, sd_varlink *link, void *userdata); diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c index 3ee4da9013b41..71f4f717f424f 100644 --- a/src/journal/journalctl.c +++ b/src/journal/journalctl.c @@ -334,6 +334,11 @@ static int vl_server(void) { if (r < 0) return log_error_errno(r, "Failed to bind Varlink methods: %m"); + /* tears down the streaming state of GetEntries follow=true calls when the client goes away */ + r = sd_varlink_server_bind_disconnect(varlink_server, vl_on_disconnect); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink disconnect handler: %m"); + r = sd_varlink_server_loop_auto(varlink_server); if (r < 0) return log_error_errno(r, "Failed to run Varlink event loop: %m"); diff --git a/src/shared/varlink-io.systemd.JournalAccess.c b/src/shared/varlink-io.systemd.JournalAccess.c index b057067079f64..6c2835d8a04ae 100644 --- a/src/shared/varlink-io.systemd.JournalAccess.c +++ b/src/shared/varlink-io.systemd.JournalAccess.c @@ -17,6 +17,8 @@ static SD_VARLINK_DEFINE_METHOD_FULL( SD_VARLINK_DEFINE_INPUT(priority, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Maximum number of entries to return. Defaults to 100, capped at 10000."), SD_VARLINK_DEFINE_INPUT(limit, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If true, do not complete after sending the newest entries: keep the call open and stream new entries as they are logged, until the client disconnects. The limit parameter then bounds only the initial backlog."), + SD_VARLINK_DEFINE_INPUT(follow, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The journal entry in flat JSON format, matching journalctl --output=json."), SD_VARLINK_DEFINE_OUTPUT(entry, SD_VARLINK_OBJECT, 0)); diff --git a/test/units/TEST-04-JOURNAL.journalctl-varlink.sh b/test/units/TEST-04-JOURNAL.journalctl-varlink.sh index d1738487f5df3..aa2e4a2a3db52 100755 --- a/test/units/TEST-04-JOURNAL.journalctl-varlink.sh +++ b/test/units/TEST-04-JOURNAL.journalctl-varlink.sh @@ -79,3 +79,56 @@ varlinkctl_get_entries '{"priority": 4, "limit": 1000}' | grep "varlink-test-war # NoEntries error is raised if there is no result (! varlinkctl call --more "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries '{"units": ["nonexistent-unit-that-should-have-no-entries.service"]}' 2>&1 | grep io.systemd.JournalAccess.NoEntries ) + +# follow mode: after the backlog the call stays open and new entries are streamed +FOLLOW_OUT="$(mktemp)" +FOLLOW_UNIT_OUT="$(mktemp)" +FOLLOW_PID="" +FOLLOW_UNIT_PID="" +cleanup_follow() { + [[ -n "$FOLLOW_PID" ]] && kill "$FOLLOW_PID" 2>/dev/null || : + [[ -n "$FOLLOW_UNIT_PID" ]] && kill "$FOLLOW_UNIT_PID" 2>/dev/null || : + rm -f "$FOLLOW_OUT" "$FOLLOW_UNIT_OUT" +} +trap cleanup_follow EXIT + +# -E is short for --more --timeout=infinity: without it varlinkctl gives up on quiet streams after 45s. +# Filter by our own unit: with debug logging the server instance traces every message it sends into +# the very journal it is following, feeding back on itself until the output queue limit (-ENOBUFS) +# kills the connection. +varlinkctl call -E "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries \ + '{"follow": true, "limit": 2, "units": ["TEST-04-JOURNAL.service"]}' >"$FOLLOW_OUT" & +FOLLOW_PID=$! + +# the backlog arrives quickly and the call does not complete +timeout 30 bash -c "until [[ \$(wc -l <'$FOLLOW_OUT') -ge 2 ]]; do sleep .5; done" +kill -0 "$FOLLOW_PID" + +# new entries are pushed into the stream as they are logged +echo "varlink-follow-live-1" | systemd-cat -t "$TAG" +journalctl --sync +timeout 30 bash -c "until grep -q varlink-follow-live-1 '$FOLLOW_OUT'; do sleep .5; done" +echo "varlink-follow-live-2" | systemd-cat -t "$TAG" +journalctl --sync +timeout 30 bash -c "until grep -q varlink-follow-live-2 '$FOLLOW_OUT'; do sleep .5; done" +kill -0 "$FOLLOW_PID" + +# follow composes with filters, and an empty backlog does not complete the call +FOLLOW_UNIT="test-journalctl-varlink-follow-$RANDOM.service" +varlinkctl call -E "$VARLINK_SOCKET" io.systemd.JournalAccess.GetEntries "{\"follow\": true, \"units\": [\"$FOLLOW_UNIT\"]}" >"$FOLLOW_UNIT_OUT" & +FOLLOW_UNIT_PID=$! +sleep 1 +kill -0 "$FOLLOW_UNIT_PID" +test ! -s "$FOLLOW_UNIT_OUT" + +systemd-run --unit="$FOLLOW_UNIT" --wait bash -c 'echo hello-from-follow-test' +journalctl --sync +timeout 30 bash -c "until grep -q hello-from-follow-test '$FOLLOW_UNIT_OUT'; do sleep .5; done" +(! grep -q varlink-follow-live-1 "$FOLLOW_UNIT_OUT") + +# on client disconnect the socket-activated server instances exit again (exit-on-idle) +kill "$FOLLOW_PID" "$FOLLOW_UNIT_PID" +FOLLOW_PID="" +FOLLOW_UNIT_PID="" +# shellcheck disable=SC2016 +timeout 30 bash -c 'until [[ $(systemctl list-units --no-legend --plain "systemd-journalctl@*.service" | wc -l) -eq 0 ]]; do sleep .5; done' From 5f6ad50d20f9bb6a87abc5ac5d43bfb9d3bf51e0 Mon Sep 17 00:00:00 2001 From: Matthew Schwartz Date: Tue, 23 Jun 2026 16:54:51 -0700 Subject: [PATCH 085/106] hwdb: add MSI Claw 8 EX AI+ CG3EM to keyboard hwdb list --- hwdb.d/60-keyboard.hwdb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hwdb.d/60-keyboard.hwdb b/hwdb.d/60-keyboard.hwdb index 097714d8b897b..08be116265cf2 100644 --- a/hwdb.d/60-keyboard.hwdb +++ b/hwdb.d/60-keyboard.hwdb @@ -1686,6 +1686,12 @@ evdev:name:AT Translated Set 2 keyboard:dmi:*:rvnMicro-StarInternationalCo.,Ltd. KEYBOARD_KEY_b9=f15 # Right Face Button KEYBOARD_KEY_ba=f16 # Left Face Button +# MSI Claw 8 EX AI+ CG3EM +# Face-button scancodes are swapped compared to the older Claw models +evdev:name:AT Translated Set 2 keyboard:dmi:*:rvnMicro-StarInternationalCo.,Ltd.:rnMS-1T91:* + KEYBOARD_KEY_db=f15 # Left Face Button + KEYBOARD_KEY_b9=f16 # Right Face Button + # MSI Katana GF66 12UD toggles touchpad using Fn+F4 where the keyboard key is 76 evdev:atkbd:dmi:*svnMicro-Star*:pnKatanaGF6612UD:* KEYBOARD_KEY_76=touchpad_toggle # Toggle touchpad From 47af1efe40a99957f183884ef849c057a598f512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Jul 2026 12:18:28 +0200 Subject: [PATCH 086/106] mkosi: update fedora commit reference to 45c16dd369c961b70664e473552def34d5469664 * 45c16dd369 Use uniform format for %rhel conditionals * 034fa693f2 Print the build status also in %build * 3cc7e03365 Restore definitions of helper macros * 2382c910b7 Disable the standalone report yet again * 2d6fd95c70 Restore explicit requires for Centos Stream 9 and 10 * 521ab0fb09 test: skip the integration test suite on Fedora ELN (for now) * 453447b79b rpminspect: ignore test-coredump-stacktrace in annocheck * 9d4edaa576 test: work around a kernel bug in virtio/vsock * c53b2fb307 test: cap the number of parallel tests * 9bd26bb71f Fix ntpvendor for ELN * de7b685908 Disable reqs for dlopen'ed libraries on CentOS * 4830641844 Move portabled to systemd-container subpackage * 893fcd9978 Add missing conditionalization and more debugging * c783e74791 split-files: improve error message * ee2dff42d6 Add systemd-report-standalone * 9c87a3f8ad Load libssl.so.4 rather than libssl.so.3 * 714b0799d2 Version 261.1 * 054158500a Update to load openssl-4 rather than openssl-3 * 5a3e750ef8 Version 261 * 4faee7ab7d Version 261~rc4 * 8ff635a921 Rebuilt for openssl 4.0 * 0064f73d97 Rebuilt for openssl 4.0 * 14a9aac87e Use dlopen notes again * 720fa8259a Do not check ownership of /var/lib/systemd/timesync/ in rpm -V * 06bd9926f2 Version 261~rc3 * 6ddbd499e8 Drop unused tree build dependency * bd81a14bfc Version 261~rc2 --- .packit.yml | 2 +- mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.packit.yml b/.packit.yml index 7a908de495b77..95a19c60bd52d 100644 --- a/.packit.yml +++ b/.packit.yml @@ -51,7 +51,7 @@ jobs: trigger: pull_request fmf_url: https://src.fedoraproject.org/rpms/systemd # This is automatically updated by tools/fetch-distro.py --update fedora - fmf_ref: 9cb09470c9c5a437f8e9c1e0e449b87de83733eb + fmf_ref: 45c16dd369c961b70664e473552def34d5469664 targets: - fedora-rawhide-x86_64 # testing-farm in the Fedora repository is explicitly configured to use testing-farm bare metal runners as diff --git a/mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf b/mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf index 50ac21fa72766..221ad4d441e43 100644 --- a/mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf +++ b/mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf @@ -9,5 +9,5 @@ Profiles=!hyperscale Environment= GIT_URL=https://src.fedoraproject.org/rpms/systemd.git GIT_BRANCH=rawhide - GIT_COMMIT=9cb09470c9c5a437f8e9c1e0e449b87de83733eb + GIT_COMMIT=45c16dd369c961b70664e473552def34d5469664 PKG_SUBDIR=fedora From 88b51ff97441041f5e0b94e60dd330964e27a2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 3 Jul 2026 12:07:02 +0200 Subject: [PATCH 087/106] meson: move 'conditions' to the end --- src/report/meson.build | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/report/meson.build b/src/report/meson.build index 8a4716ec21c0a..cc2cea521e41b 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -39,13 +39,13 @@ executables += [ }, libexec_template + { 'name' : 'systemd-report-sign-plain', - 'conditions' : [ - 'HAVE_OPENSSL', - ], 'sources' : files( 'report-sign-plain.c', ), 'dependencies' : libopenssl_cflags, + 'conditions' : [ + 'HAVE_OPENSSL', + ], }, libexec_template + { 'name' : 'systemd-report-sign-tsm', From f00ccf93b75e0b434554d0690ec0e57dd5c71d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 3 Jul 2026 12:30:09 +0200 Subject: [PATCH 088/106] meson: rename extract+objects to export+import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have two concepts: a list of files that should be extracted from an intermediate build artifact to be used in a later build artifact, and for a given artifact, a list "donors". The first list was called "extract", because it is passed to the .extract_objects() meson function, and the second was called "objects", because that is the meson parameter to specify pre-built objects files (6350d2dbd97746440b9c8303ddc140ffda568732). But this naming is confusing: we don't care about the 'extract' step, this is something internal to the build machinery. And 'objects' is a very generic term. Let's use 'export' for the stuff that is "exported" for other binaries to use, and 'import' to say where to import from. Those terms are symmetrical and the association between them should be intuitive. (If you think the terms are actually assymetrical, there are precedents for confusing the import with the import sources. E.g. in modern English, turkeys are called so because they were imported from the Americas and guineafowls were imported from Africa via Türkiye and all that foreign stuff is alike.) --- meson.build | 20 ++++++++++---------- src/analyze/meson.build | 4 ++-- src/busctl/meson.build | 4 ++-- src/coredump/meson.build | 4 ++-- src/hibernate-resume/meson.build | 4 ++-- src/home/meson.build | 10 +++++----- src/imds/meson.build | 6 +++--- src/import/meson.build | 16 ++++++++-------- src/integritysetup/meson.build | 4 ++-- src/journal-remote/meson.build | 10 +++++----- src/journal/meson.build | 12 ++++++------ src/libsystemd-network/meson.build | 6 +++--- src/locale/meson.build | 4 ++-- src/login/meson.build | 4 ++-- src/machine/meson.build | 4 ++-- src/network/meson.build | 10 +++++----- src/nspawn/meson.build | 4 ++-- src/nsresourced/meson.build | 6 +++--- src/oom/meson.build | 4 ++-- src/repart/meson.build | 4 ++-- src/resolve/meson.build | 8 ++++---- src/shutdown/meson.build | 4 ++-- src/sleep/meson.build | 4 ++-- src/ssh-generator/meson.build | 4 ++-- src/systemctl/meson.build | 4 ++-- src/sysupdate/meson.build | 6 +++--- src/sysusers/meson.build | 4 ++-- src/test/meson.build | 10 +++++----- src/timesync/meson.build | 6 +++--- src/tmpfiles/meson.build | 4 ++-- src/udev/meson.build | 10 +++++----- src/vmspawn/meson.build | 6 +++--- src/xdg-autostart-generator/meson.build | 6 +++--- 33 files changed, 108 insertions(+), 108 deletions(-) diff --git a/meson.build b/meson.build index 1ce4c95368da3..0c54ef2408691 100644 --- a/meson.build +++ b/meson.build @@ -1815,7 +1815,7 @@ catalogs = [] modules = [] # nss, pam, and other plugins executables = [] executables_by_name = {} -objects_by_name = {} +exported_by_name = {} fuzzer_exes = [] sources = [] @@ -2276,7 +2276,7 @@ foreach dict : executables continue endif - exe_sources = dict.get('sources', []) + dict.get('extract', []) + exe_sources = dict.get('sources', []) + dict.get('export', []) foreach bpf_name : dict.get('bpf_programs', []) if bpf_name in bpf_programs_by_name @@ -2287,7 +2287,7 @@ foreach dict : executables kwargs = {} foreach key, val : dict if key in ['name', 'dbus', 'public', 'conditions', 'type', 'suite', - 'timeout', 'parallel', 'objects', 'sources', 'extract', + 'timeout', 'parallel', 'sources', 'import', 'export', 'include_directories', 'build_by_default', 'install', 'bpf_programs'] continue @@ -2305,9 +2305,9 @@ foreach dict : executables include_directories += fs.parent(exe_sources[0]) endif - foreach val : dict.get('objects', []) - obj = objects_by_name[val] - kwargs += { 'objects' : kwargs.get('objects', []) + obj['objects'] } + foreach val : dict.get('import', []) + obj = exported_by_name[val] + kwargs += { 'objects' : kwargs.get('objects', []) + obj['exported'] } include_directories += obj['include_directories'] endforeach @@ -2354,11 +2354,11 @@ foreach dict : executables sources += exe_sources endif - if dict.has_key('extract') - objects_by_name += { + if dict.has_key('export') + exported_by_name += { name : { - 'objects' : exe.extract_objects(dict['extract']), - 'include_directories' : fs.parent(dict['extract'][0]), + 'exported' : exe.extract_objects(dict['export']), + 'include_directories' : fs.parent(dict['export'][0]), } } endif diff --git a/src/analyze/meson.build b/src/analyze/meson.build index a8232eb851cd2..bd8dc7ccdd586 100644 --- a/src/analyze/meson.build +++ b/src/analyze/meson.build @@ -49,7 +49,7 @@ executables += [ 'name' : 'systemd-analyze', 'public' : conf.get('ENABLE_ANALYZE') == 1, 'sources' : systemd_analyze_sources, - 'extract' : systemd_analyze_extract_sources, + 'export' : systemd_analyze_extract_sources, 'include_directories' : core_includes, 'link_with' : [ libcore, @@ -63,6 +63,6 @@ executables += [ }, core_test_template + { 'sources' : files('test-verify.c'), - 'objects' : ['systemd-analyze'], + 'import' : ['systemd-analyze'], }, ] diff --git a/src/busctl/meson.build b/src/busctl/meson.build index 9aee5b0d252eb..d878bf518685a 100644 --- a/src/busctl/meson.build +++ b/src/busctl/meson.build @@ -12,10 +12,10 @@ executables += [ 'name' : 'busctl', 'public' : true, 'sources' : busctl_sources, - 'extract' : busctl_extract_sources, + 'export' : busctl_extract_sources, }, test_template + { 'sources' : files('test-busctl-introspect.c'), - 'objects' : ['busctl'], + 'import' : ['busctl'], }, ] diff --git a/src/coredump/meson.build b/src/coredump/meson.build index b1286f80ff679..b8f6e4d97bda8 100644 --- a/src/coredump/meson.build +++ b/src/coredump/meson.build @@ -36,7 +36,7 @@ executables += [ libexec_template + { 'name' : 'systemd-coredump', 'sources' : systemd_coredump_sources, - 'extract' : systemd_coredump_extract_sources, + 'export' : systemd_coredump_extract_sources, 'link_with' : [libshared], 'dependencies' : [ common_dependencies, @@ -52,7 +52,7 @@ executables += [ }, test_template + { 'sources' : files('test-coredump-vacuum.c'), - 'objects' : ['systemd-coredump'], + 'import' : ['systemd-coredump'], 'type' : 'manual', }, ] diff --git a/src/hibernate-resume/meson.build b/src/hibernate-resume/meson.build index e18f60e3b47bb..b864d9fd84e0f 100644 --- a/src/hibernate-resume/meson.build +++ b/src/hibernate-resume/meson.build @@ -8,11 +8,11 @@ executables += [ libexec_template + { 'name' : 'systemd-hibernate-resume', 'sources' : files('hibernate-resume.c'), - 'extract' : files('hibernate-resume-config.c'), + 'export' : files('hibernate-resume-config.c'), }, generator_template + { 'name' : 'systemd-hibernate-resume-generator', 'sources' : files('hibernate-resume-generator.c'), - 'objects' : ['systemd-hibernate-resume'], + 'import' : ['systemd-hibernate-resume'], }, ] diff --git a/src/home/meson.build b/src/home/meson.build index 2713b0463514b..693a5e706f246 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -66,13 +66,13 @@ executables += [ 'name' : 'systemd-homed', 'dbus' : true, 'sources' : systemd_homed_sources, - 'extract' : systemd_homed_extract_sources, + 'export' : systemd_homed_extract_sources, 'dependencies' : libopenssl_cflags, }, libexec_template + { 'name' : 'systemd-homework', 'sources' : systemd_homework_sources, - 'objects' : ['systemd-homed'], + 'import' : ['systemd-homed'], 'dependencies' : [ libblkid_cflags, libcryptsetup_cflags, @@ -86,8 +86,8 @@ executables += [ 'name' : 'homectl', 'public' : true, 'sources' : homectl_sources, - 'extract' : homectl_extract, - 'objects' : ['systemd-homed'], + 'export' : homectl_extract, + 'import' : ['systemd-homed'], 'dependencies' : [ libfido2_cflags, libopenssl_cflags, @@ -96,7 +96,7 @@ executables += [ }, test_template + { 'sources' : files('test-homectl-prompts.c'), - 'objects' : ['homectl'], + 'import' : ['homectl'], 'type' : 'manual', }, test_template + { diff --git a/src/imds/meson.build b/src/imds/meson.build index 9167600b651ef..ca54acd3f31e6 100644 --- a/src/imds/meson.build +++ b/src/imds/meson.build @@ -9,7 +9,7 @@ executables += [ 'name' : 'systemd-imdsd', 'public' : true, 'sources' : files('imdsd.c'), - 'extract' : files('imds-util.c'), + 'export' : files('imds-util.c'), 'dependencies' : [ libcurl_cflags, ], @@ -20,12 +20,12 @@ executables += [ 'sources' : files( 'imds-tool.c', 'imds-tool-metrics.c'), - 'objects' : ['systemd-imdsd'], + 'import' : ['systemd-imdsd'], }, generator_template + { 'name' : 'systemd-imds-generator', 'sources' : files('imds-generator.c'), - 'objects' : ['systemd-imdsd'], + 'import' : ['systemd-imdsd'], }, ] diff --git a/src/import/meson.build b/src/import/meson.build index e141ffb9750f5..54caa928dce98 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -12,7 +12,7 @@ executables += [ 'importd.c', 'oci-util.c', ), - 'extract' : files( + 'export' : files( 'oci-util.c', 'import-common.c', 'qcow2-util.c', @@ -33,7 +33,7 @@ executables += [ 'pull-raw.c', 'pull-tar.c', ), - 'objects' : ['systemd-importd'], + 'import' : ['systemd-importd'], 'dependencies' : [ libcurl_cflags, libopenssl_cflags, @@ -47,7 +47,7 @@ executables += [ 'import-raw.c', 'import-tar.c', ), - 'objects' : ['systemd-importd'], + 'import' : ['systemd-importd'], }, libexec_template + { 'name' : 'systemd-import-fs', @@ -55,7 +55,7 @@ executables += [ 'sources' : files( 'import-fs.c', ), - 'objects' : ['systemd-importd'], + 'import' : ['systemd-importd'], }, libexec_template + { 'name' : 'systemd-export', @@ -65,13 +65,13 @@ executables += [ 'export-tar.c', 'export-raw.c', ), - 'objects' : ['systemd-importd'], + 'import' : ['systemd-importd'], }, executable_template + { 'name' : 'importctl', 'public' : true, 'sources' : files('importctl.c'), - 'objects': ['systemd-importd'], + 'import' : ['systemd-importd'], }, generator_template + { 'name' : 'systemd-import-generator', @@ -86,12 +86,12 @@ executables += [ }, test_template + { 'sources' : files('test-qcow2.c'), - 'objects' : ['systemd-importd'], + 'import' : ['systemd-importd'], 'type' : 'manual', }, test_template + { 'sources' : files('test-oci-util.c'), - 'objects': ['systemd-importd'], + 'import' : ['systemd-importd'], }, ] diff --git a/src/integritysetup/meson.build b/src/integritysetup/meson.build index 4f3601e681938..7f29c031b8d4a 100644 --- a/src/integritysetup/meson.build +++ b/src/integritysetup/meson.build @@ -8,12 +8,12 @@ executables += [ libexec_template + { 'name' : 'systemd-integritysetup', 'sources' : files('integritysetup.c'), - 'extract' : files('integrity-util.c'), + 'export' : files('integrity-util.c'), 'dependencies' : libcryptsetup_cflags, }, generator_template + { 'name' : 'systemd-integritysetup-generator', 'sources' : files('integritysetup-generator.c'), - 'objects' : ['systemd-integritysetup'], + 'import' : ['systemd-integritysetup'], }, ] diff --git a/src/journal-remote/meson.build b/src/journal-remote/meson.build index 1338a3e673cef..a69858665f6f1 100644 --- a/src/journal-remote/meson.build +++ b/src/journal-remote/meson.build @@ -46,7 +46,7 @@ executables += [ # Instead, we make sure we don't install it when the remote feature is disabled. 'install' : conf.get('ENABLE_REMOTE') == 1, 'sources' : systemd_journal_remote_sources, - 'extract' : systemd_journal_remote_extract_sources, + 'export' : systemd_journal_remote_extract_sources, 'dependencies' : common_deps + [libmicrohttpd_cflags], }, libexec_template + { @@ -57,8 +57,8 @@ executables += [ 'HAVE_LIBCURL', ], 'sources' : systemd_journal_upload_sources, - 'extract' : systemd_journal_upload_extract_sources, - 'objects' : ['systemd-journal-remote'], + 'export' : systemd_journal_upload_extract_sources, + 'import' : ['systemd-journal-remote'], 'dependencies' : [ common_deps, libcurl_cflags, @@ -67,11 +67,11 @@ executables += [ test_template + { 'sources' : files('test-journal-header-util.c'), 'conditions' : ['ENABLE_REMOTE', 'HAVE_LIBCURL'], - 'objects' : ['systemd-journal-upload'], + 'import' : ['systemd-journal-upload'], }, fuzz_template + { 'sources' : files('fuzz-journal-remote.c'), - 'objects' : ['systemd-journal-remote'], + 'import' : ['systemd-journal-remote'], 'dependencies' : common_deps + [libmicrohttpd_cflags], }, ] diff --git a/src/journal/meson.build b/src/journal/meson.build index 41561deb8b3b7..703e9c9fe437c 100644 --- a/src/journal/meson.build +++ b/src/journal/meson.build @@ -78,11 +78,11 @@ else endif journal_test_template = test_template + { - 'objects' : ['systemd-journald'], + 'import' : ['systemd-journald'], } journal_fuzz_template = fuzz_template + { - 'objects' : [ + 'import' : [ 'fuzz-journald-audit', 'systemd-journald', ], @@ -93,7 +93,7 @@ executables += [ libexec_template + { 'name' : 'systemd-journald', 'sources' : systemd_journald_sources, - 'extract' : systemd_journald_extract_sources, + 'export' : systemd_journald_extract_sources, 'dependencies' : [ libacl_cflags, libaudit_cflags, @@ -117,7 +117,7 @@ executables += [ 'name' : 'systemd-cat', 'public' : true, 'sources' : files('cat.c'), - 'objects' : ['systemd-journald'], + 'import' : ['systemd-journald'], }, executable_template + { 'name' : 'journalctl', @@ -168,8 +168,8 @@ executables += [ fuzz_template + { 'sources' : files('fuzz-journald-audit.c'), # fuzz-journald-util.c is shared with the other fuzzers below. - 'extract' : files('fuzz-journald-util.c'), - 'objects' : ['systemd-journald'], + 'export' : files('fuzz-journald-util.c'), + 'import' : ['systemd-journald'], }, journal_fuzz_template + { 'sources' : files('fuzz-journald-kmsg.c'), diff --git a/src/libsystemd-network/meson.build b/src/libsystemd-network/meson.build index 7340bed03855f..2ac7d29fffe0a 100644 --- a/src/libsystemd-network/meson.build +++ b/src/libsystemd-network/meson.build @@ -116,11 +116,11 @@ executables += [ }, network_test_template + { 'sources' : files('test-ndisc-ra.c'), - 'extract' : files('icmp6-test-util.c'), + 'export' : files('icmp6-test-util.c'), }, network_test_template + { 'sources' : files('test-ndisc-rs.c'), - 'objects' : ['test-ndisc-ra'], + 'import' : ['test-ndisc-ra'], }, network_test_template + { 'sources' : files('test-ndisc-send.c'), @@ -146,6 +146,6 @@ executables += [ }, network_fuzz_template + { 'sources' : files('fuzz-ndisc-rs.c'), - 'objects' : ['test-ndisc-ra'], + 'import' : ['test-ndisc-ra'], }, ] diff --git a/src/locale/meson.build b/src/locale/meson.build index 2f99bb8d8072a..fa3651ace074e 100644 --- a/src/locale/meson.build +++ b/src/locale/meson.build @@ -29,7 +29,7 @@ executables += [ 'name' : 'systemd-localed', 'dbus' : true, 'sources' : systemd_localed_sources, - 'extract' : systemd_localed_extract_sources, + 'export' : systemd_localed_extract_sources, 'dependencies' : libxkbcommon_deps, }, executable_template + { @@ -39,7 +39,7 @@ executables += [ }, test_template + { 'sources' : files('test-localed-util.c'), - 'objects' : ['systemd-localed'], + 'import' : ['systemd-localed'], 'dependencies' : libxkbcommon_deps, }, ] diff --git a/src/login/meson.build b/src/login/meson.build index effb44c3a8314..e718e1f44f2f2 100644 --- a/src/login/meson.build +++ b/src/login/meson.build @@ -47,7 +47,7 @@ executables += [ 'name' : 'systemd-logind', 'dbus' : true, 'sources' : systemd_logind_sources, - 'extract' : systemd_logind_extract_sources, + 'export' : systemd_logind_extract_sources, 'dependencies' : [ libacl_cflags, ], @@ -77,7 +77,7 @@ executables += [ }, test_template + { 'sources' : files('test-login-tables.c'), - 'objects' : ['systemd-logind'], + 'import' : ['systemd-logind'], }, test_template + { 'sources' : files('test-session-properties.c'), diff --git a/src/machine/meson.build b/src/machine/meson.build index fc16e9f5c5f32..40cab032ecd90 100644 --- a/src/machine/meson.build +++ b/src/machine/meson.build @@ -26,7 +26,7 @@ executables += [ 'name' : 'systemd-machined', 'dbus' : true, 'sources' : systemd_machined_sources, - 'extract' : systemd_machined_extract_sources, + 'export' : systemd_machined_extract_sources, }, executable_template + { 'name' : 'machinectl', @@ -40,7 +40,7 @@ executables += [ }, test_template + { 'sources' : files('test-machine-tables.c'), - 'objects' : ['systemd-machined'], + 'import' : ['systemd-machined'], }, ] diff --git a/src/network/meson.build b/src/network/meson.build index b620e512df776..53e2fa1496267 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -178,7 +178,7 @@ network_test_template = test_template + { networkd_link_with, libsystemd_network, ], - 'objects' : ['systemd-networkd'], + 'import' : ['systemd-networkd'], 'include_directories' : network_includes, } @@ -188,7 +188,7 @@ network_fuzz_template = fuzz_template + { networkd_link_with, libsystemd_network, ], - 'objects' : ['systemd-networkd'], + 'import' : ['systemd-networkd'], 'include_directories' : network_includes, } @@ -198,7 +198,7 @@ executables += [ 'dbus' : true, 'conditions' : ['ENABLE_NETWORKD'], 'sources' : systemd_networkd_sources, - 'extract' : systemd_networkd_extract_sources, + 'export' : systemd_networkd_extract_sources, 'include_directories' : network_includes, 'link_with' : [ libsystemd_network, @@ -235,12 +235,12 @@ executables += [ libexec_template + { 'name' : 'systemd-network-generator', 'sources' : files('generator/network-generator-main.c'), - 'extract' : files('generator/network-generator.c'), + 'export' : files('generator/network-generator.c'), 'link_with' : networkd_link_with, }, test_template + { 'sources' : files('generator/test-network-generator.c'), - 'objects' : ['systemd-network-generator'], + 'import' : ['systemd-network-generator'], 'suite' : 'network', }, network_test_template + { diff --git a/src/nspawn/meson.build b/src/nspawn/meson.build index 95bc461cc5a08..c362aff4ac033 100644 --- a/src/nspawn/meson.build +++ b/src/nspawn/meson.build @@ -31,7 +31,7 @@ nspawn_extract_sources += nspawn_gperf_c nspawn_common_template = { 'dependencies' : libseccomp_cflags, - 'objects' : ['systemd-nspawn'], + 'import' : ['systemd-nspawn'], } nspawn_test_template = test_template + nspawn_common_template nspawn_fuzz_template = fuzz_template + nspawn_common_template @@ -41,7 +41,7 @@ executables += [ 'name' : 'systemd-nspawn', 'public' : true, 'sources' : nspawn_sources, - 'extract' : nspawn_extract_sources, + 'export' : nspawn_extract_sources, 'include_directories' : [ include_directories('.'), executable_template['include_directories'], diff --git a/src/nsresourced/meson.build b/src/nsresourced/meson.build index 2037d06bc8a41..1e950250bbcdf 100644 --- a/src/nsresourced/meson.build +++ b/src/nsresourced/meson.build @@ -23,7 +23,7 @@ executables += [ libexec_template + { 'name' : 'systemd-nsresourced', 'sources' : systemd_nsresourced_sources, - 'extract' : systemd_nsresourced_extract_sources, + 'export' : systemd_nsresourced_extract_sources, 'dependencies' : [ libbpf_cflags, ], @@ -32,13 +32,13 @@ executables += [ libexec_template + { 'name' : 'systemd-nsresourcework', 'sources' : systemd_nsresourcework_sources, - 'objects' : ['systemd-nsresourced'], + 'import' : ['systemd-nsresourced'], 'bpf_programs': ['userns-restrict'], }, test_template + { 'sources' : test_userns_restrict_sources, 'conditions' : ['HAVE_VMLINUX_H'], - 'objects' : ['systemd-nsresourced'], + 'import' : ['systemd-nsresourced'], 'bpf_programs': ['userns-restrict'], }, ] diff --git a/src/oom/meson.build b/src/oom/meson.build index 95541e5542c6a..96ab9bd823729 100644 --- a/src/oom/meson.build +++ b/src/oom/meson.build @@ -19,7 +19,7 @@ executables += [ 'name' : 'systemd-oomd', 'dbus' : true, 'sources' : systemd_oomd_sources, - 'extract' : systemd_oomd_extract_sources, + 'export' : systemd_oomd_extract_sources, 'dependencies' : libatomic, }, executable_template + { @@ -29,7 +29,7 @@ executables += [ }, test_template + { 'sources' : files('test-oomd-util.c'), - 'objects' : ['systemd-oomd'], + 'import' : ['systemd-oomd'], 'dependencies' : libatomic, }, ] diff --git a/src/repart/meson.build b/src/repart/meson.build index 721fe73b9ae9e..d7c2edf0d532f 100644 --- a/src/repart/meson.build +++ b/src/repart/meson.build @@ -8,7 +8,7 @@ executables += [ executable_template + { 'name' : 'systemd-repart', 'public' : true, - 'extract' : files( + 'export' : files( 'iso9660.c', 'repart.c', 'repart-list-candidate-devices.c', @@ -25,7 +25,7 @@ executables += [ executable_template + { 'name' : 'systemd-repart.standalone', 'public' : true, - 'objects' : ['systemd-repart'], + 'import' : ['systemd-repart'], 'link_with' : [ libc_wrapper_static, libbasic_static, diff --git a/src/resolve/meson.build b/src/resolve/meson.build index 019c4b7bb06c4..1e0e7e30f396e 100644 --- a/src/resolve/meson.build +++ b/src/resolve/meson.build @@ -74,15 +74,15 @@ resolve_common_template = { ], } -resolve_test_template = test_template + resolve_common_template + {'objects' : ['systemd-resolved']} -resolve_fuzz_template = fuzz_template + resolve_common_template + {'objects' : ['systemd-resolved']} +resolve_test_template = test_template + resolve_common_template + {'import' : ['systemd-resolved']} +resolve_fuzz_template = fuzz_template + resolve_common_template + {'import' : ['systemd-resolved']} executables += [ libexec_template + resolve_common_template + { 'name' : 'systemd-resolved', 'dbus' : true, 'sources' : systemd_resolved_sources, - 'extract' : systemd_resolved_extract_sources, + 'export' : systemd_resolved_extract_sources, }, executable_template + resolve_common_template + { 'name' : 'resolvectl', @@ -91,7 +91,7 @@ executables += [ 'resolvconf-compat.c', 'resolvectl.c', ), - 'objects' : ['systemd-resolved'], + 'import' : ['systemd-resolved'], }, resolve_test_template + { 'sources' : files('test-resolve-tables.c'), diff --git a/src/shutdown/meson.build b/src/shutdown/meson.build index 709b0fa69257d..98669bfdf3fbe 100644 --- a/src/shutdown/meson.build +++ b/src/shutdown/meson.build @@ -12,7 +12,7 @@ shutdown_detach_sources = files( executables += [ libexec_template + { 'name' : 'systemd-shutdown', - 'extract' : files( + 'export' : files( 'detach-dm.c', 'detach-loopback.c', 'detach-md.c', @@ -22,7 +22,7 @@ executables += [ }, libexec_template + { 'name' : 'systemd-shutdown.standalone', - 'objects' : ['systemd-shutdown'], + 'import' : ['systemd-shutdown'], 'link_with' : [ libc_wrapper_static, libbasic_static, diff --git a/src/sleep/meson.build b/src/sleep/meson.build index 7411aa1cebda0..663a36e038da5 100644 --- a/src/sleep/meson.build +++ b/src/sleep/meson.build @@ -4,11 +4,11 @@ executables += [ libexec_template + { 'name' : 'systemd-sleep', 'sources' : files('sleep.c'), - 'extract' : files('battery-capacity.c'), + 'export' : files('battery-capacity.c'), }, test_template + { 'sources' : files('test-battery-capacity.c'), - 'objects' : ['systemd-sleep'], + 'import' : ['systemd-sleep'], }, ] diff --git a/src/ssh-generator/meson.build b/src/ssh-generator/meson.build index f6babb47d3a4b..65e34cfba8b36 100644 --- a/src/ssh-generator/meson.build +++ b/src/ssh-generator/meson.build @@ -4,7 +4,7 @@ executables += [ generator_template + { 'name' : 'systemd-ssh-generator', 'sources' : files('ssh-generator.c'), - 'extract' : files('ssh-util.c'), + 'export' : files('ssh-util.c'), }, libexec_template + { 'name' : 'systemd-ssh-proxy', @@ -13,7 +13,7 @@ executables += [ libexec_template + { 'name' : 'systemd-ssh-issue', 'sources' : files('ssh-issue.c'), - 'objects' : ['systemd-ssh-generator'], + 'import' : ['systemd-ssh-generator'], }, ] diff --git a/src/systemctl/meson.build b/src/systemctl/meson.build index c287f8a490384..150435989da43 100644 --- a/src/systemctl/meson.build +++ b/src/systemctl/meson.build @@ -52,7 +52,7 @@ executables += [ 'name' : 'systemctl', 'public' : true, 'sources' : systemctl_sources, - 'extract' : systemctl_extract_sources, + 'export' : systemctl_extract_sources, 'link_with' : systemctl_link_with, 'dependencies' : [ liblz4_cflags, @@ -63,7 +63,7 @@ executables += [ }, fuzz_template + { 'sources' : files('fuzz-systemctl-parse-argv.c'), - 'objects' : ['systemctl'], + 'import' : ['systemctl'], 'link_with' : systemctl_link_with, 'dependencies' : [ libselinux_cflags, diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build index ec873269047e8..f13e70b49faf5 100644 --- a/src/sysupdate/meson.build +++ b/src/sysupdate/meson.build @@ -29,7 +29,7 @@ executables += [ 'public' : true, 'conditions' : ['ENABLE_SYSUPDATE'], 'sources' : systemd_sysupdate_sources, - 'extract' : systemd_sysupdate_extract_sources, + 'export' : systemd_sysupdate_extract_sources, 'dependencies' : [ libfdisk_cflags, libopenssl_cflags, @@ -45,12 +45,12 @@ executables += [ 'name' : 'updatectl', 'public' : true, 'sources' : systemd_updatectl_sources, - 'objects' : ['systemd-sysupdate'], + 'import' : ['systemd-sysupdate'], 'conditions' : ['ENABLE_SYSUPDATED'], }, test_template + { 'sources' : files('test-sysupdate-util.c'), - 'objects' : ['systemd-sysupdate'], + 'import' : ['systemd-sysupdate'], 'conditions' : ['ENABLE_SYSUPDATE'], }, ] diff --git a/src/sysusers/meson.build b/src/sysusers/meson.build index b74ac6aa1a8c4..9ff149d72d2d0 100644 --- a/src/sysusers/meson.build +++ b/src/sysusers/meson.build @@ -8,13 +8,13 @@ executables += [ executable_template + { 'name' : 'systemd-sysusers', 'public' : true, - 'extract' : files('sysusers.c'), + 'export' : files('sysusers.c'), 'dependencies' : libaudit_cflags, }, executable_template + { 'name' : 'systemd-sysusers.standalone', 'public' : true, - 'objects' : ['systemd-sysusers'], + 'import' : ['systemd-sysusers'], 'link_with' : [ libc_wrapper_static, libbasic_static, diff --git a/src/test/meson.build b/src/test/meson.build index d7353ce8e2ce5..2cb18a798a888 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -438,14 +438,14 @@ executables += [ }, test_template + { 'sources' : files('test-nss-hosts.c'), - 'extract' : files('nss-test-util.c'), + 'export' : files('nss-test-util.c'), 'dependencies' : libseccomp_cflags, 'conditions' : ['ENABLE_NSS'], 'timeout' : 120, }, test_template + { 'sources' : files('test-nss-users.c'), - 'objects' : ['test-nss-hosts'], + 'import' : ['test-nss-hosts'], 'conditions' : ['ENABLE_NSS'], }, test_template + { @@ -463,7 +463,7 @@ executables += [ }, test_template + { 'sources' : files('test-qmp-client-qemu.c'), - 'objects' : ['systemd-vmspawn'], + 'import' : ['systemd-vmspawn'], 'conditions' : ['ENABLE_VMSPAWN'], }, test_template + { @@ -517,12 +517,12 @@ executables += [ }, test_template + { 'sources' : files('test-varlink-idl-machine.c'), - 'objects' : ['systemd-machined'], + 'import' : ['systemd-machined'], 'conditions' : ['ENABLE_MACHINED'], }, test_template + { 'sources' : files('test-varlink-idl-login.c'), - 'objects' : ['systemd-logind'], + 'import' : ['systemd-logind'], 'conditions' : ['ENABLE_LOGIND'], }, test_template + { diff --git a/src/timesync/meson.build b/src/timesync/meson.build index c42fcd3e70963..d52b16ef7a539 100644 --- a/src/timesync/meson.build +++ b/src/timesync/meson.build @@ -33,19 +33,19 @@ executables += [ libexec_template + { 'name' : 'systemd-timesyncd', 'sources' : timesyncd_sources, - 'extract' : timesyncd_extract_sources, + 'export' : timesyncd_extract_sources, 'link_with' : timesyncd_link_with, 'dependencies' : libm, }, libexec_template + { 'name' : 'systemd-time-wait-sync', 'sources' : files('wait-sync.c'), - 'objects' : ['systemd-timesyncd'], + 'import' : ['systemd-timesyncd'], 'dependencies' : libm, }, test_template + { 'sources' : files('test-timesync.c'), - 'objects' : ['systemd-timesyncd'], + 'import' : ['systemd-timesyncd'], 'dependencies' : libm, }, ] diff --git a/src/tmpfiles/meson.build b/src/tmpfiles/meson.build index fcf2749e4bf2b..5f661c19259de 100644 --- a/src/tmpfiles/meson.build +++ b/src/tmpfiles/meson.build @@ -10,7 +10,7 @@ executables += [ executable_template + { 'name' : 'systemd-tmpfiles', 'public' : true, - 'extract' : files('tmpfiles.c') + + 'export' : files('tmpfiles.c') + offline_passwd_c, 'dependencies' : [ libacl_cflags, @@ -20,7 +20,7 @@ executables += [ executable_template + { 'name' : 'systemd-tmpfiles.standalone', 'public' : true, - 'objects' : ['systemd-tmpfiles'], + 'import' : ['systemd-tmpfiles'], 'link_with' : [ libc_wrapper_static, libbasic_static, diff --git a/src/udev/meson.build b/src/udev/meson.build index 6c2232c74c6dc..da386ae9c167b 100644 --- a/src/udev/meson.build +++ b/src/udev/meson.build @@ -135,7 +135,7 @@ udev_plugin_template = executable_template + { } udev_common_template = { - 'objects' : ['udevadm'], + 'import' : ['udevadm'], 'dependencies' : [ libacl_cflags, libblkid_cflags, @@ -150,7 +150,7 @@ udev_binaries_dict = [ 'public' : true, 'sources' : udevadm_sources + keyboard_keys_from_name_inc, - 'extract' : udevadm_extract_sources, + 'export' : udevadm_extract_sources, 'include_directories' : [ include_directories('.', 'net'), libexec_template['include_directories'], @@ -176,7 +176,7 @@ udev_binaries_dict = [ udev_plugin_template + { 'name' : 'fido_id', 'sources' : files('fido_id/fido_id.c'), - 'extract' : files('fido_id/fido_id_desc.c'), + 'export' : files('fido_id/fido_id_desc.c'), }, udev_plugin_template + { 'name' : 'iocost', @@ -211,7 +211,7 @@ executables += udev_binaries_dict executables += [ test_template + { 'sources' : files('fido_id/test-fido-id-desc.c'), - 'objects' : ['fido_id'], + 'import' : ['fido_id'], 'suite' : 'udev', }, udev_test_template + { @@ -245,7 +245,7 @@ executables += [ }, fuzz_template + { 'sources' : files('fido_id/fuzz-fido-id-desc.c'), - 'objects' : ['fido_id'], + 'import' : ['fido_id'], }, udev_fuzz_template + { 'sources' : files('net/fuzz-link-parser.c'), diff --git a/src/vmspawn/meson.build b/src/vmspawn/meson.build index 76617cc6d46e3..f804ff892607d 100644 --- a/src/vmspawn/meson.build +++ b/src/vmspawn/meson.build @@ -23,14 +23,14 @@ executables += [ 'name' : 'systemd-vmspawn', 'public' : true, 'sources' : vmspawn_sources, - 'extract' : vmspawn_extract_sources, + 'export' : vmspawn_extract_sources, }, test_template + { 'sources' : files('test-vmspawn-util.c'), - 'objects' : ['systemd-vmspawn'], + 'import' : ['systemd-vmspawn'], }, test_template + { 'sources' : files('test-vmspawn-qemu-config.c'), - 'objects' : ['systemd-vmspawn'], + 'import' : ['systemd-vmspawn'], }, ] diff --git a/src/xdg-autostart-generator/meson.build b/src/xdg-autostart-generator/meson.build index 752ee9f8abf6c..110fe3971d229 100644 --- a/src/xdg-autostart-generator/meson.build +++ b/src/xdg-autostart-generator/meson.build @@ -8,7 +8,7 @@ executables += [ executable_template + { 'name' : 'systemd-xdg-autostart-generator', 'sources' : files('xdg-autostart-generator.c'), - 'extract' : files('xdg-autostart-service.c'), + 'export' : files('xdg-autostart-service.c'), 'install_dir' : usergeneratordir, }, libexec_template + { @@ -17,10 +17,10 @@ executables += [ }, test_template + { 'sources' : files('test-xdg-autostart.c'), - 'objects' : ['systemd-xdg-autostart-generator'], + 'import' : ['systemd-xdg-autostart-generator'], }, fuzz_template + { 'sources' : files('fuzz-xdg-desktop.c'), - 'objects' : ['systemd-xdg-autostart-generator'], + 'import' : ['systemd-xdg-autostart-generator'], }, ] From aad1e6101029b0a6f9fb47fb66a918beec2d5475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 3 Jul 2026 13:41:53 +0200 Subject: [PATCH 089/106] meson: adjust naming of '*extract*' variables --- src/analyze/meson.build | 4 ++-- src/busctl/meson.build | 4 ++-- src/coredump/meson.build | 4 ++-- src/home/meson.build | 8 ++++---- src/journal-remote/meson.build | 8 ++++---- src/journal/meson.build | 8 ++++---- src/locale/meson.build | 4 ++-- src/login/meson.build | 6 +++--- src/machine/meson.build | 4 ++-- src/network/meson.build | 6 +++--- src/nspawn/meson.build | 6 +++--- src/nsresourced/meson.build | 4 ++-- src/oom/meson.build | 4 ++-- src/resolve/meson.build | 8 ++++---- src/systemctl/meson.build | 4 ++-- src/sysupdate/meson.build | 4 ++-- src/timesync/meson.build | 6 +++--- src/udev/meson.build | 14 +++++++------- src/vmspawn/meson.build | 4 ++-- 19 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/analyze/meson.build b/src/analyze/meson.build index bd8dc7ccdd586..2b8b260c47f84 100644 --- a/src/analyze/meson.build +++ b/src/analyze/meson.build @@ -40,7 +40,7 @@ systemd_analyze_sources = files( 'analyze-verify.c', 'analyze.c', ) -systemd_analyze_extract_sources = files( +systemd_analyze_export_sources = files( 'analyze-verify-util.c' ) @@ -49,7 +49,7 @@ executables += [ 'name' : 'systemd-analyze', 'public' : conf.get('ENABLE_ANALYZE') == 1, 'sources' : systemd_analyze_sources, - 'export' : systemd_analyze_extract_sources, + 'export' : systemd_analyze_export_sources, 'include_directories' : core_includes, 'link_with' : [ libcore, diff --git a/src/busctl/meson.build b/src/busctl/meson.build index d878bf518685a..5395e1420f77e 100644 --- a/src/busctl/meson.build +++ b/src/busctl/meson.build @@ -3,7 +3,7 @@ busctl_sources = files( 'busctl.c', ) -busctl_extract_sources = files( +busctl_export_sources = files( 'busctl-introspect.c', ) @@ -12,7 +12,7 @@ executables += [ 'name' : 'busctl', 'public' : true, 'sources' : busctl_sources, - 'export' : busctl_extract_sources, + 'export' : busctl_export_sources, }, test_template + { 'sources' : files('test-busctl-introspect.c'), diff --git a/src/coredump/meson.build b/src/coredump/meson.build index b8f6e4d97bda8..2ae293ae9949b 100644 --- a/src/coredump/meson.build +++ b/src/coredump/meson.build @@ -14,7 +14,7 @@ systemd_coredump_sources = files( 'coredump-send.c', 'coredump-submit.c', ) -systemd_coredump_extract_sources = files( +systemd_coredump_export_sources = files( 'coredump-vacuum.c', ) @@ -36,7 +36,7 @@ executables += [ libexec_template + { 'name' : 'systemd-coredump', 'sources' : systemd_coredump_sources, - 'export' : systemd_coredump_extract_sources, + 'export' : systemd_coredump_export_sources, 'link_with' : [libshared], 'dependencies' : [ common_dependencies, diff --git a/src/home/meson.build b/src/home/meson.build index 693a5e706f246..668a4f7774dcc 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -16,7 +16,7 @@ systemd_homed_sources = files( 'homed.c', 'user-record-sign.c', ) -systemd_homed_extract_sources = files( +systemd_homed_export_sources = files( 'home-util.c', 'user-record-password-quality.c', 'user-record-util.c', @@ -51,7 +51,7 @@ homectl_sources = files( 'homectl.c', ) -homectl_extract = files( +homectl_export_sources = files( 'homectl-prompts.c', ) @@ -66,7 +66,7 @@ executables += [ 'name' : 'systemd-homed', 'dbus' : true, 'sources' : systemd_homed_sources, - 'export' : systemd_homed_extract_sources, + 'export' : systemd_homed_export_sources, 'dependencies' : libopenssl_cflags, }, libexec_template + { @@ -86,7 +86,7 @@ executables += [ 'name' : 'homectl', 'public' : true, 'sources' : homectl_sources, - 'export' : homectl_extract, + 'export' : homectl_export_sources, 'import' : ['systemd-homed'], 'dependencies' : [ libfido2_cflags, diff --git a/src/journal-remote/meson.build b/src/journal-remote/meson.build index a69858665f6f1..ecd7e6dc4c6a7 100644 --- a/src/journal-remote/meson.build +++ b/src/journal-remote/meson.build @@ -5,7 +5,7 @@ systemd_journal_gatewayd_sources = files( ) systemd_journal_remote_sources = files('journal-remote-main.c') -systemd_journal_remote_extract_sources = files( +systemd_journal_remote_export_sources = files( 'journal-compression-util.c', 'journal-remote-parse.c', 'journal-remote-write.c', @@ -16,7 +16,7 @@ systemd_journal_upload_sources = files( 'journal-upload-journal.c', 'journal-upload.c', ) -systemd_journal_upload_extract_sources = files( +systemd_journal_upload_export_sources = files( 'journal-header-util.c', ) @@ -46,7 +46,7 @@ executables += [ # Instead, we make sure we don't install it when the remote feature is disabled. 'install' : conf.get('ENABLE_REMOTE') == 1, 'sources' : systemd_journal_remote_sources, - 'export' : systemd_journal_remote_extract_sources, + 'export' : systemd_journal_remote_export_sources, 'dependencies' : common_deps + [libmicrohttpd_cflags], }, libexec_template + { @@ -57,7 +57,7 @@ executables += [ 'HAVE_LIBCURL', ], 'sources' : systemd_journal_upload_sources, - 'export' : systemd_journal_upload_extract_sources, + 'export' : systemd_journal_upload_export_sources, 'import' : ['systemd-journal-remote'], 'dependencies' : [ common_deps, diff --git a/src/journal/meson.build b/src/journal/meson.build index 703e9c9fe437c..4ddc92484a0c0 100644 --- a/src/journal/meson.build +++ b/src/journal/meson.build @@ -3,7 +3,7 @@ systemd_journald_sources = files( 'journald.c', ) -systemd_journald_extract_sources = files( +systemd_journald_export_sources = files( 'audit-type.c', 'journald-audit.c', 'journald-client.c', @@ -28,7 +28,7 @@ journald_gperf_c = custom_target( command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) generated_sources += journald_gperf_c -systemd_journald_extract_sources += journald_gperf_c +systemd_journald_export_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] @@ -53,7 +53,7 @@ audit_type_to_name = custom_target( capture : true) generated_sources += audit_type_to_name -systemd_journald_extract_sources += audit_type_to_name +systemd_journald_export_sources += audit_type_to_name journalctl_sources = files( 'journalctl.c', @@ -93,7 +93,7 @@ executables += [ libexec_template + { 'name' : 'systemd-journald', 'sources' : systemd_journald_sources, - 'export' : systemd_journald_extract_sources, + 'export' : systemd_journald_export_sources, 'dependencies' : [ libacl_cflags, libaudit_cflags, diff --git a/src/locale/meson.build b/src/locale/meson.build index fa3651ace074e..04fd5b1d5d2ec 100644 --- a/src/locale/meson.build +++ b/src/locale/meson.build @@ -7,7 +7,7 @@ endif systemd_localed_sources = files( 'localed.c' ) -systemd_localed_extract_sources = files( +systemd_localed_export_sources = files( 'localed-util.c', 'xkbcommon-util.c', ) @@ -29,7 +29,7 @@ executables += [ 'name' : 'systemd-localed', 'dbus' : true, 'sources' : systemd_localed_sources, - 'export' : systemd_localed_extract_sources, + 'export' : systemd_localed_export_sources, 'dependencies' : libxkbcommon_deps, }, executable_template + { diff --git a/src/login/meson.build b/src/login/meson.build index e718e1f44f2f2..bfb09a209309f 100644 --- a/src/login/meson.build +++ b/src/login/meson.build @@ -7,7 +7,7 @@ endif systemd_logind_sources = files( 'logind.c', ) -systemd_logind_extract_sources = files( +systemd_logind_export_sources = files( 'logind-action.c', 'logind-brightness.c', 'logind-button.c', @@ -35,7 +35,7 @@ logind_gperf_c = custom_target( command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) generated_sources += logind_gperf_c -systemd_logind_extract_sources += [logind_gperf_c] +systemd_logind_export_sources += [logind_gperf_c] loginctl_sources = files( 'loginctl.c', @@ -47,7 +47,7 @@ executables += [ 'name' : 'systemd-logind', 'dbus' : true, 'sources' : systemd_logind_sources, - 'export' : systemd_logind_extract_sources, + 'export' : systemd_logind_export_sources, 'dependencies' : [ libacl_cflags, ], diff --git a/src/machine/meson.build b/src/machine/meson.build index 40cab032ecd90..71dd5042de706 100644 --- a/src/machine/meson.build +++ b/src/machine/meson.build @@ -7,7 +7,7 @@ endif systemd_machined_sources = files( 'machined.c' ) -systemd_machined_extract_sources = files( +systemd_machined_export_sources = files( 'image.c', 'image-dbus.c', 'image-varlink.c', @@ -26,7 +26,7 @@ executables += [ 'name' : 'systemd-machined', 'dbus' : true, 'sources' : systemd_machined_sources, - 'export' : systemd_machined_extract_sources, + 'export' : systemd_machined_export_sources, }, executable_template + { 'name' : 'machinectl', diff --git a/src/network/meson.build b/src/network/meson.build index 53e2fa1496267..8baa80556fdb7 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -3,7 +3,7 @@ systemd_networkd_sources = files( 'networkd.c' ) -systemd_networkd_extract_sources = files( +systemd_networkd_export_sources = files( 'netdev/bareudp.c', 'netdev/batadv.c', 'netdev/bond.c', @@ -161,7 +161,7 @@ netdev_gperf_c = custom_target( command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) generated_sources += [networkd_gperf_c, networkd_network_gperf_c, netdev_gperf_c] -systemd_networkd_extract_sources += [networkd_gperf_c, networkd_network_gperf_c, netdev_gperf_c] +systemd_networkd_export_sources += [networkd_gperf_c, networkd_network_gperf_c, netdev_gperf_c] if get_option('link-networkd-shared') networkd_link_with = [libshared] @@ -198,7 +198,7 @@ executables += [ 'dbus' : true, 'conditions' : ['ENABLE_NETWORKD'], 'sources' : systemd_networkd_sources, - 'export' : systemd_networkd_extract_sources, + 'export' : systemd_networkd_export_sources, 'include_directories' : network_includes, 'link_with' : [ libsystemd_network, diff --git a/src/nspawn/meson.build b/src/nspawn/meson.build index c362aff4ac033..ddf859c63d5d0 100644 --- a/src/nspawn/meson.build +++ b/src/nspawn/meson.build @@ -13,7 +13,7 @@ nspawn_sources = files( 'nspawn-setuid.c', 'nspawn-stub-pid1.c', ) -nspawn_extract_sources = files( +nspawn_export_sources = files( 'nspawn-expose-ports.c', 'nspawn-mount.c', 'nspawn-network.c', @@ -27,7 +27,7 @@ nspawn_gperf_c = custom_target( command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) generated_sources += nspawn_gperf_c -nspawn_extract_sources += nspawn_gperf_c +nspawn_export_sources += nspawn_gperf_c nspawn_common_template = { 'dependencies' : libseccomp_cflags, @@ -41,7 +41,7 @@ executables += [ 'name' : 'systemd-nspawn', 'public' : true, 'sources' : nspawn_sources, - 'export' : nspawn_extract_sources, + 'export' : nspawn_export_sources, 'include_directories' : [ include_directories('.'), executable_template['include_directories'], diff --git a/src/nsresourced/meson.build b/src/nsresourced/meson.build index 1e950250bbcdf..13f64277499f5 100644 --- a/src/nsresourced/meson.build +++ b/src/nsresourced/meson.build @@ -8,7 +8,7 @@ systemd_nsresourced_sources = files( 'nsresourced-manager.c', 'nsresourced.c', ) -systemd_nsresourced_extract_sources = files( +systemd_nsresourced_export_sources = files( 'userns-restrict.c', 'userns-registry.c', ) @@ -23,7 +23,7 @@ executables += [ libexec_template + { 'name' : 'systemd-nsresourced', 'sources' : systemd_nsresourced_sources, - 'export' : systemd_nsresourced_extract_sources, + 'export' : systemd_nsresourced_export_sources, 'dependencies' : [ libbpf_cflags, ], diff --git a/src/oom/meson.build b/src/oom/meson.build index 96ab9bd823729..2c29ff1ee7a56 100644 --- a/src/oom/meson.build +++ b/src/oom/meson.build @@ -10,7 +10,7 @@ systemd_oomd_sources = files( 'oomd-manager.c', 'oomd-manager-bus.c', ) -systemd_oomd_extract_sources = files( +systemd_oomd_export_sources = files( 'oomd-util.c', ) @@ -19,7 +19,7 @@ executables += [ 'name' : 'systemd-oomd', 'dbus' : true, 'sources' : systemd_oomd_sources, - 'export' : systemd_oomd_extract_sources, + 'export' : systemd_oomd_export_sources, 'dependencies' : libatomic, }, executable_template + { diff --git a/src/resolve/meson.build b/src/resolve/meson.build index 1e0e7e30f396e..3181ace1cb2ce 100644 --- a/src/resolve/meson.build +++ b/src/resolve/meson.build @@ -7,7 +7,7 @@ endif systemd_resolved_sources = files( 'resolved.c', ) -systemd_resolved_extract_sources = files( +systemd_resolved_export_sources = files( 'resolved-bus.c', 'resolved-conf.c', 'resolved-dns-browse-services.c', @@ -59,10 +59,10 @@ resolved_dns_delegate_gperf_c = custom_target( command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) generated_sources += [resolved_gperf_c, resolved_dnssd_gperf_c, resolved_dns_delegate_gperf_c] -systemd_resolved_extract_sources += [resolved_gperf_c, resolved_dnssd_gperf_c, resolved_dns_delegate_gperf_c] +systemd_resolved_export_sources += [resolved_gperf_c, resolved_dnssd_gperf_c, resolved_dns_delegate_gperf_c] if conf.get('ENABLE_DNS_OVER_TLS') == 1 - systemd_resolved_extract_sources += files( + systemd_resolved_export_sources += files( 'resolved-dnstls.c', ) endif @@ -82,7 +82,7 @@ executables += [ 'name' : 'systemd-resolved', 'dbus' : true, 'sources' : systemd_resolved_sources, - 'export' : systemd_resolved_extract_sources, + 'export' : systemd_resolved_export_sources, }, executable_template + resolve_common_template + { 'name' : 'resolvectl', diff --git a/src/systemctl/meson.build b/src/systemctl/meson.build index 150435989da43..7b75eef43c1e3 100644 --- a/src/systemctl/meson.build +++ b/src/systemctl/meson.build @@ -30,7 +30,7 @@ systemctl_sources = files( 'systemctl-trivial-method.c', 'systemctl-whoami.c', ) -systemctl_extract_sources = files( +systemctl_export_sources = files( 'systemctl-compat-halt.c', 'systemctl-compat-shutdown.c', 'systemctl-daemon-reload.c', @@ -52,7 +52,7 @@ executables += [ 'name' : 'systemctl', 'public' : true, 'sources' : systemctl_sources, - 'export' : systemctl_extract_sources, + 'export' : systemctl_export_sources, 'link_with' : systemctl_link_with, 'dependencies' : [ liblz4_cflags, diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build index f13e70b49faf5..664f439f797e9 100644 --- a/src/sysupdate/meson.build +++ b/src/sysupdate/meson.build @@ -14,7 +14,7 @@ systemd_sysupdate_sources = files( 'sysupdate-update-set.c', 'sysupdate.c', ) -systemd_sysupdate_extract_sources = files( +systemd_sysupdate_export_sources = files( 'sysupdate-update-set-flags.c', 'sysupdate-util.c', ) @@ -29,7 +29,7 @@ executables += [ 'public' : true, 'conditions' : ['ENABLE_SYSUPDATE'], 'sources' : systemd_sysupdate_sources, - 'export' : systemd_sysupdate_extract_sources, + 'export' : systemd_sysupdate_export_sources, 'dependencies' : [ libfdisk_cflags, libopenssl_cflags, diff --git a/src/timesync/meson.build b/src/timesync/meson.build index d52b16ef7a539..896a51c191a12 100644 --- a/src/timesync/meson.build +++ b/src/timesync/meson.build @@ -8,7 +8,7 @@ timesyncd_sources = files( 'timesyncd.c', 'timesyncd-bus.c', ) -timesyncd_extract_sources = files( +timesyncd_export_sources = files( 'timesyncd-conf.c', 'timesyncd-manager.c', 'timesyncd-server.c', @@ -20,7 +20,7 @@ timesyncd_gperf_c = custom_target( command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) generated_sources += timesyncd_gperf_c -timesyncd_extract_sources += timesyncd_gperf_c +timesyncd_export_sources += timesyncd_gperf_c if get_option('link-timesyncd-shared') timesyncd_link_with = [libshared] @@ -33,7 +33,7 @@ executables += [ libexec_template + { 'name' : 'systemd-timesyncd', 'sources' : timesyncd_sources, - 'export' : timesyncd_extract_sources, + 'export' : timesyncd_export_sources, 'link_with' : timesyncd_link_with, 'dependencies' : libm, }, diff --git a/src/udev/meson.build b/src/udev/meson.build index da386ae9c167b..cfbf958ed3821 100644 --- a/src/udev/meson.build +++ b/src/udev/meson.build @@ -17,7 +17,7 @@ udevadm_sources = files( 'udevadm.c', 'udevd.c', ) -udevadm_extract_sources = files( +udevadm_export_sources = files( 'net/link-config.c', 'udev-builtin.c', 'udev-builtin-btrfs.c', @@ -48,19 +48,19 @@ udevadm_extract_sources = files( ) if conf.get('HAVE_KMOD') == 1 - udevadm_extract_sources += files('udev-builtin-kmod.c') + udevadm_export_sources += files('udev-builtin-kmod.c') endif if conf.get('HAVE_BLKID') == 1 - udevadm_extract_sources += files('udev-builtin-blkid.c') + udevadm_export_sources += files('udev-builtin-blkid.c') endif if conf.get('HAVE_ACL') == 1 - udevadm_extract_sources += files('udev-builtin-uaccess.c') + udevadm_export_sources += files('udev-builtin-uaccess.c') endif if conf.get('HAVE_TPM2') == 1 - udevadm_extract_sources += files('udev-builtin-tpm2_id.c') + udevadm_export_sources += files('udev-builtin-tpm2_id.c') endif ############################################################ @@ -101,7 +101,7 @@ link_config_gperf_c = custom_target( output : 'link-config-gperf.c', command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) -udevadm_extract_sources += link_config_gperf_c +udevadm_export_sources += link_config_gperf_c generated_sources += link_config_gperf_c ############################################################ @@ -150,7 +150,7 @@ udev_binaries_dict = [ 'public' : true, 'sources' : udevadm_sources + keyboard_keys_from_name_inc, - 'export' : udevadm_extract_sources, + 'export' : udevadm_export_sources, 'include_directories' : [ include_directories('.', 'net'), libexec_template['include_directories'], diff --git a/src/vmspawn/meson.build b/src/vmspawn/meson.build index f804ff892607d..8838939229787 100644 --- a/src/vmspawn/meson.build +++ b/src/vmspawn/meson.build @@ -13,7 +13,7 @@ vmspawn_sources = files( 'vmspawn-scope.c', 'vmspawn-mount.c', ) -vmspawn_extract_sources = files( +vmspawn_export_sources = files( 'vmspawn-util.c', 'vmspawn-qemu-config.c', ) @@ -23,7 +23,7 @@ executables += [ 'name' : 'systemd-vmspawn', 'public' : true, 'sources' : vmspawn_sources, - 'export' : vmspawn_extract_sources, + 'export' : vmspawn_export_sources, }, test_template + { 'sources' : files('test-vmspawn-util.c'), From 9155c0a5b9e65153b86b738682d2827f1c8761a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 3 Jul 2026 13:28:36 +0200 Subject: [PATCH 090/106] meson: build .standalone versions of the report binaries The goal is to be allow the use of those binaries on systemd with older systemd. The report stuff is generally independent of the running systemd version. When built with -Dinstall-tests=true -Dstandalone-binaries=true -Dbuildtype=release -Db_lto=true, gcc-16.1.1-2.fc44.x86_64, the sizes are quite reasonable: $ ls -lG build-lto/*.standalone -rwxr-xr-x 1 zbyszek 713200 Jul 3 13:16 build-lto/systemd-report.standalone -rwxr-xr-x 1 zbyszek 662936 Jul 3 13:16 build-lto/systemd-report-basic.standalone -rwxr-xr-x 1 zbyszek 567424 Jul 3 13:16 build-lto/systemd-report-cgroup.standalone -rwxr-xr-x 1 zbyszek 592856 Jul 3 13:16 build-lto/systemd-report-files.standalone -rwxr-xr-x 1 zbyszek 589864 Jul 3 13:16 build-lto/systemd-report-sign-plain.standalone -rwxr-xr-x 1 zbyszek 548624 Jul 3 13:16 build-lto/systemd-report-sign-tsm.standalone --- src/report/meson.build | 56 ++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/src/report/meson.build b/src/report/meson.build index cc2cea521e41b..2c49a97fbf64f 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -1,10 +1,10 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -executables += [ - libexec_template + { +report_executables = [ + { 'name' : 'systemd-report', 'public' : true, - 'sources' : files( + 'export' : files( 'report.c', 'report-generate.c', 'report-sign.c', @@ -14,43 +14,63 @@ executables += [ libcurl_cflags, ], }, - - libexec_template + { + { 'name' : 'systemd-report-basic', 'public' : true, - 'sources' : files( - 'report-basic-server.c', + 'export' : files( 'report-basic.c', + 'report-basic-server.c', ), }, - libexec_template + { + { 'name' : 'systemd-report-cgroup', - 'sources' : files( + 'export' : files( 'report-cgroup.c', 'report-cgroup-server.c', ), }, - libexec_template + { + { 'name' : 'systemd-report-files', - 'sources' : files( + 'export' : files( 'report-files.c', 'report-files-server.c', ), }, - libexec_template + { + { 'name' : 'systemd-report-sign-plain', - 'sources' : files( + 'export' : files( 'report-sign-plain.c', ), 'dependencies' : libopenssl_cflags, - 'conditions' : [ - 'HAVE_OPENSSL', - ], + 'conditions' : ['HAVE_OPENSSL'], }, - libexec_template + { + { 'name' : 'systemd-report-sign-tsm', - 'sources' : files( + 'export' : files( 'report-sign-tsm.c', ), }, ] + +foreach exe : report_executables + exe2 = { + 'name' : exe['name'] + '.standalone', + 'import' : [exe['name']], + 'link_with' : [ + libc_wrapper_static, + libbasic_static, + libshared_static, + libsystemd_static, + ] + } + foreach optional : ['public', 'dependencies', 'conditions'] + if optional in exe + exe2 += { optional : exe[optional] } + endif + endforeach + + executables += [ + libexec_template + exe, + libexec_template + exe2, + ] +endforeach From f731334d8491de2ca7948ad798574a12cd303fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 3 Jul 2026 15:32:02 +0200 Subject: [PATCH 091/106] meson: use modern syntax for dict checks --- meson.build | 4 ++-- test/meson.build | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 0c54ef2408691..81bf99f415312 100644 --- a/meson.build +++ b/meson.build @@ -2354,7 +2354,7 @@ foreach dict : executables sources += exe_sources endif - if dict.has_key('export') + if 'export' in dict exported_by_name += { name : { 'exported' : exe.extract_objects(dict['export']), @@ -2718,7 +2718,7 @@ if meson.version().version_compare('>=1.4.0') uniq = {} foreach source : sources - if uniq.has_key(source.full_path()) + if source.full_path() in uniq continue endif uniq += {source.full_path(): source} diff --git a/test/meson.build b/test/meson.build index fd3dba3a60093..f3a8541f8e614 100644 --- a/test/meson.build +++ b/test/meson.build @@ -98,7 +98,7 @@ endif ############################################################ test_compare_versions_sh = files('test-compare-versions.sh') -if want_tests != 'false' and executables_by_name.has_key('systemd-analyze') +if want_tests != 'false' and 'systemd-analyze' in executables_by_name exe = executables_by_name.get('systemd-analyze') test('test-compare-versions', test_compare_versions_sh, @@ -156,7 +156,7 @@ endif ############################################################ test_fstab_generator_sh = files('test-fstab-generator.sh') -if want_tests != 'false' and executables_by_name.has_key('systemd-fstab-generator') +if want_tests != 'false' and 'systemd-fstab-generator' in executables_by_name exe = executables_by_name.get('systemd-fstab-generator') test('test-fstab-generator', test_fstab_generator_sh, From e785d81c1b4b62394a07f83429a7852fbdb37629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Jul 2026 18:37:12 +0200 Subject: [PATCH 092/106] mkosi: update fedora commit reference to 57cbcf979ce2dd872a871c59e87d2c65dfa996e6 * 57cbcf979c Also enable report-standalone for OBS builds * d69c17b165 Enable report-standalone for upstream builds --- .packit.yml | 2 +- mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.packit.yml b/.packit.yml index 95a19c60bd52d..482997c3769b3 100644 --- a/.packit.yml +++ b/.packit.yml @@ -51,7 +51,7 @@ jobs: trigger: pull_request fmf_url: https://src.fedoraproject.org/rpms/systemd # This is automatically updated by tools/fetch-distro.py --update fedora - fmf_ref: 45c16dd369c961b70664e473552def34d5469664 + fmf_ref: 57cbcf979ce2dd872a871c59e87d2c65dfa996e6 targets: - fedora-rawhide-x86_64 # testing-farm in the Fedora repository is explicitly configured to use testing-farm bare metal runners as diff --git a/mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf b/mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf index 221ad4d441e43..1f919d56766dd 100644 --- a/mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf +++ b/mkosi/mkosi.pkgenv/mkosi.conf.d/centos-fedora.conf @@ -9,5 +9,5 @@ Profiles=!hyperscale Environment= GIT_URL=https://src.fedoraproject.org/rpms/systemd.git GIT_BRANCH=rawhide - GIT_COMMIT=45c16dd369c961b70664e473552def34d5469664 + GIT_COMMIT=57cbcf979ce2dd872a871c59e87d2c65dfa996e6 PKG_SUBDIR=fedora From 366fa466d2a791b3f989953b83b58307d5e6961a Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Fri, 3 Jul 2026 14:44:40 +0800 Subject: [PATCH 093/106] copy: keep replaced target after publish errors Once link_tmpfile_at() succeeds, the target path has been published. Avoid unlinking it on later close or parent fsync failures when COPY_REPLACE was used, as that can remove the replaced target. Signed-off-by: dongshengyuan --- src/shared/copy.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/shared/copy.c b/src/shared/copy.c index f61b16caee0b4..21add1d5c0dec 100644 --- a/src/shared/copy.c +++ b/src/shared/copy.c @@ -1645,7 +1645,11 @@ int copy_file_atomic_at_full( return 0; fail: - (void) unlinkat(dir_fdt, to, 0); + /* link_tmpfile_at() succeeded, so 'to' is now published. In replacement mode, do not + * remove it again, as that may delete a pre-existing target replaced by this copy. */ + if (!FLAGS_SET(copy_flags, COPY_REPLACE)) + (void) unlinkat(dir_fdt, to, 0); + return r; } From d8c1f1d105e6313fabf843276bbc5c980f619e43 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Fri, 3 Jul 2026 15:17:53 +0800 Subject: [PATCH 094/106] pull: honor sync for OCI artifacts Synchronize OCI layer directories and generated metadata when IMPORT_SYNC is enabled, matching the raw and tar pull paths. Signed-off-by: dongshengyuan --- src/import/pull-oci.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/import/pull-oci.c b/src/import/pull-oci.c index a9019f19a51c0..90dfee9ed4a84 100644 --- a/src/import/pull-oci.c +++ b/src/import/pull-oci.c @@ -1067,7 +1067,9 @@ static int oci_pull_save_nspawn_settings(OciPull *i) { fprintf(f, "Parameters=%s\n", ej); } - r = flink_tmpfile(f, tmpfile, j, LINK_TMPFILE_REPLACE); + r = flink_tmpfile(f, tmpfile, j, + LINK_TMPFILE_REPLACE| + (i->flags & IMPORT_SYNC ? LINK_TMPFILE_SYNC : 0)); if (r < 0) return log_error_errno(r, "Failed to move '%s' into place: %m", j); @@ -1099,7 +1101,9 @@ static int oci_pull_save_oci_config(OciPull *i) { if (r < 0) return log_error_errno(r, "Failed to write '%s': %m", j); - r = link_tmpfile(fd, tmpfile, j, LINK_TMPFILE_REPLACE); + r = link_tmpfile(fd, tmpfile, j, + LINK_TMPFILE_REPLACE| + (i->flags & IMPORT_SYNC ? LINK_TMPFILE_SYNC : 0)); if (r < 0) return log_error_errno(r, "Failed to move '%s' into place: %m", j); @@ -1174,8 +1178,13 @@ static int oci_pull_save_mstack(OciPull *i) { } } - if (rename(jt, j) < 0) - return log_error_errno(errno, "Failed to move '%s' into place: %m", j); + r = install_file( + AT_FDCWD, jt, + AT_FDCWD, j, + (i->flags & IMPORT_FORCE ? INSTALL_REPLACE : 0) | + (i->flags & IMPORT_SYNC ? INSTALL_SYNCFS|INSTALL_GRACEFUL : 0)); + if (r < 0) + return log_error_errno(r, "Failed to move '%s' into place: %m", j); jt = mfree(jt); /* Disarm rm_rf_physical_and_free() */ From 93aadbf968c0408ce9dea468e22ac0baba527763 Mon Sep 17 00:00:00 2001 From: Vsevolod Kozlov Date: Mon, 6 Jul 2026 13:40:53 +0300 Subject: [PATCH 095/106] sbsign: write unaligned signature size into WIN_CERTIFICATE header The inclusion of padding bytes in the signature size can lead to the signature being rejected by strict PKCS7 parsers. Meanwhile, according to [1], the parser of the WIN_CERTIFICATE structure is expected to round up the value of dwLength to an 8-byte multiple. This also matches the behaviour of the sbsign tool from sbsigntools. Fixes #42884 [1] https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only Signed-off-by: Vsevolod Kozlov --- src/sbsign/sbsign.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sbsign/sbsign.c b/src/sbsign/sbsign.c index 5408ffc61526e..e7a519404c8a8 100644 --- a/src/sbsign/sbsign.c +++ b/src/sbsign/sbsign.c @@ -633,7 +633,7 @@ static int verb_sign(int argc, char *argv[], uintptr_t _data, void *userdata) { &(WIN_CERTIFICATE_HEADER) { .wRevision = htole16(0x200), .wCertificateType = htole16(0x0002), /* PKCS7 signedData */ - .dwLength = htole32(ROUND_UP(certsz, 8)), + .dwLength = htole32(certsz), }, sizeof(WIN_CERTIFICATE_HEADER), end); From a977bf4d06d57652f44b1e8e3d5de700bb977434 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Sat, 4 Jul 2026 17:05:33 +0800 Subject: [PATCH 096/106] portable: detect drop-in-only attachments Commit edea370222 (portable: remove drop-in configs even if the main unit file does not exist) taught detach to handle leftover .service.d directories after the main unit symlink was removed. portable_get_state_internal() still had the same blind spot: it only considered regular unit entries, so portablectl is-attached could report detached while a portable drop-in directory was still present. Handle those drop-in-only entries like detach does, preserve the existing unit-file based enabled checks when the main unit file is still present, and add TEST-29 coverage. Signed-off-by: dongshengyuan --- src/portable/portable.c | 101 ++++++++++++++++------- test/units/TEST-29-PORTABLE.directory.sh | 10 ++- 2 files changed, 78 insertions(+), 33 deletions(-) diff --git a/src/portable/portable.c b/src/portable/portable.c index f0bdb40fd22f3..c1abe16985d7a 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -2295,6 +2295,42 @@ static int test_chroot_dropin( return r; } +static int portable_attached_dirent_name( + const struct dirent *de, + char **ret_unit_name, + bool *ret_dropin) { + + _cleanup_free_ char *unit_name = NULL; + const char *dropin_suffix; + + assert(de); + assert(ret_unit_name); + + /* When a portable service is enabled with "portablectl --copy=symlink --enable --now attach", + * and is disabled with "portablectl --enable --now detach", which calls DisableUnitFilesWithFlags + * DBus method, the main unit file is removed, but its drop-ins are not. Hence, we need to list both + * main unit files and drop-in directories (without the main unit files). */ + + dropin_suffix = endswith(de->d_name, ".d"); + if (dropin_suffix) + unit_name = strndup(de->d_name, dropin_suffix - de->d_name); + else + unit_name = strdup(de->d_name); + if (!unit_name) + return -ENOMEM; + + if (!unit_name_is_valid(unit_name, UNIT_NAME_ANY)) + return 0; + + if (dropin_suffix ? !IN_SET(de->d_type, DT_LNK, DT_DIR) : !IN_SET(de->d_type, DT_LNK, DT_REG)) + return 0; + + *ret_unit_name = TAKE_PTR(unit_name); + if (ret_dropin) + *ret_dropin = dropin_suffix != NULL; + return 1; +} + int portable_detach( RuntimeScope scope, sd_bus *bus, @@ -2331,31 +2367,17 @@ int portable_detach( FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) { _cleanup_free_ char *marker = NULL, *unit_name = NULL; - const char *dot; - - /* When a portable service is enabled with "portablectl --copy=symlink --enable --now attach", - * and is disabled with "portablectl --enable --now detach", which calls DisableUnitFilesWithFlags - * DBus method, the main unit file is removed, but its drop-ins are not. Hence, here we need - * to list both main unit files and drop-in directories (without the main unit files). */ - dot = endswith(de->d_name, ".d"); - if (dot) - unit_name = strndup(de->d_name, dot - de->d_name); - else - unit_name = strdup(de->d_name); - if (!unit_name) - return -ENOMEM; - - if (!unit_name_is_valid(unit_name, UNIT_NAME_ANY)) + r = portable_attached_dirent_name(de, &unit_name, /* ret_dropin= */ NULL); + if (r < 0) + return r; + if (r == 0) continue; /* Filter out duplicates */ if (set_contains(unit_files, unit_name)) continue; - if (dot ? !IN_SET(de->d_type, DT_LNK, DT_DIR) : !IN_SET(de->d_type, DT_LNK, DT_REG)) - continue; - r = test_chroot_dropin(d, where, unit_name, name_or_path, extension_image_paths, &marker); if (r < 0) return r; @@ -2521,39 +2543,54 @@ static int portable_get_state_internal( } FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) { - UnitFileState state; + _cleanup_free_ char *unit_name = NULL; + bool dropin; - if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY)) + r = portable_attached_dirent_name(de, &unit_name, &dropin); + if (r < 0) + return r; + if (r == 0) continue; /* Filter out duplicates */ - if (set_contains(unit_files, de->d_name)) + if (set_contains(unit_files, unit_name)) continue; - if (!IN_SET(de->d_type, DT_LNK, DT_REG)) - continue; + if (dropin) { + /* If the main unit file still exists, let the regular entry handle it so that + * enabled/running state is determined from the unit file as before. */ + r = RET_NERRNO(faccessat(dirfd(d), unit_name, F_OK, AT_SYMLINK_NOFOLLOW)); + if (r >= 0) + continue; + if (r != -ENOENT) + return log_debug_errno(r, "Failed to check if '%s/%s' exists: %m", where, unit_name); + } - r = test_chroot_dropin(d, where, de->d_name, name_or_path, extension_image_paths, NULL); + r = test_chroot_dropin(d, where, unit_name, name_or_path, extension_image_paths, NULL); if (r < 0) return r; if (r == 0) continue; - r = unit_file_lookup_state(scope, &paths, de->d_name, &state); - if (r < 0) - return log_debug_errno(r, "Failed to determine unit file state of '%s': %m", de->d_name); - if (!IN_SET(state, UNIT_FILE_STATIC, UNIT_FILE_DISABLED, UNIT_FILE_LINKED, UNIT_FILE_LINKED_RUNTIME)) - found_enabled = true; + if (!dropin) { + UnitFileState state; - r = unit_file_is_active(bus, de->d_name, error); + r = unit_file_lookup_state(scope, &paths, unit_name, &state); + if (r < 0) + return log_debug_errno(r, "Failed to determine unit file state of '%s': %m", unit_name); + if (!IN_SET(state, UNIT_FILE_STATIC, UNIT_FILE_DISABLED, UNIT_FILE_LINKED, UNIT_FILE_LINKED_RUNTIME)) + found_enabled = true; + } + + r = unit_file_is_active(bus, unit_name, error); if (r < 0) return r; if (r > 0) found_running = true; - r = set_put_strdup(&unit_files, de->d_name); + r = set_ensure_consume(&unit_files, &string_hash_ops_free, TAKE_PTR(unit_name)); if (r < 0) - return log_debug_errno(r, "Failed to add unit name '%s' to set: %m", de->d_name); + return log_oom_debug(); } *ret = found_running ? (!set_isempty(unit_files) && (flags & PORTABLE_RUNTIME) ? PORTABLE_RUNNING_RUNTIME : PORTABLE_RUNNING) : diff --git a/test/units/TEST-29-PORTABLE.directory.sh b/test/units/TEST-29-PORTABLE.directory.sh index 8f35c9ee08382..42f280b78ad2c 100755 --- a/test/units/TEST-29-PORTABLE.directory.sh +++ b/test/units/TEST-29-PORTABLE.directory.sh @@ -144,9 +144,17 @@ portablectl detach --now --runtime --extension /tmp/app0 /tmp/rootdir app0 # Provides coverage for https://github.com/systemd/systemd/issues/23481 portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/rootdir minimal-app0 portablectl detach --now --runtime --enable /tmp/rootdir minimal-app0 -# attach and detach again to check if all drop-in configs are removed even if the main unit files are removed +# Attach and detach again to check if drop-in-only leftovers are still recognized and removed. portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/rootdir minimal-app0 +rm /run/systemd/system.attached/minimal-app0*.service +status="$(portablectl is-attached --runtime /tmp/rootdir)" +[[ "${status}" == "running-runtime" ]] +systemctl stop minimal-app0.service minimal-app0-foo.service +status="$(portablectl is-attached --runtime /tmp/rootdir)" +[[ "${status}" == "attached-runtime" ]] portablectl detach --now --runtime --enable /tmp/rootdir minimal-app0 +[[ ! -d /run/systemd/system.attached/minimal-app0.service.d ]] +[[ ! -d /run/systemd/system.attached/minimal-app0-foo.service.d ]] # The wrong file should be ignored, given the right one has the xattr set trap 'rm -rf /var/cache/wrongext' EXIT From 626161a24c794fcea09719b66390783420ed38a0 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Tue, 7 Jul 2026 18:31:53 +0200 Subject: [PATCH 097/106] gitignore: add .envrc, .direnv Signed-off-by: Paul Meyer --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 34104ff0e9aad..aa60e04b22615 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ __pycache__/ /pkg/ .aider* /worktrees +.envrc +.direnv/ From d9c0d7974e14ad04aa9bc53af4d2e3c0d4c20fe8 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Tue, 7 Jul 2026 18:12:44 +0200 Subject: [PATCH 098/106] sysinstall: Disable timeout for connection with repart The repart connection was timeing out when fetching candidate devices. Therfore disable the timeout and keep the connection open till the user disconnects. --- src/sysinstall/sysinstall.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c index eca9681b378cf..e1346a5e26ffe 100644 --- a/src/sysinstall/sysinstall.c +++ b/src/sysinstall/sysinstall.c @@ -1875,6 +1875,12 @@ static int vl_method_list_candidate_devices( if (r < 0) return r; + /* Disable connection timeout so that the connection to repart doesn't close before the link is + * disconnected */ + r = sd_varlink_set_relative_timeout(context->repart_link, UINT64_MAX); + 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)); From 231fddc337db380bc95aa9c8f661534dffbea8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Sj=C3=B6strom?= Date: Thu, 2 Jul 2026 10:08:03 +0200 Subject: [PATCH 099/106] core: support stdio fd passing in io.systemd.Unit.StartTransient Add StandardInputFileDescriptor, StandardOutputFileDescriptor and StandardErrorFileDescriptor to the Service context of the Varlink StartTransient() method. Each carries the push-order index of a file descriptor passed along with the method call (via SCM_RIGHTS), and connects it to the transient service's standard input/output/error. This is the Varlink equivalent of the StandardInputFileDescriptor= / StandardOutputFileDescriptor= / StandardErrorFileDescriptor= D-Bus transient properties behind "systemd-run --pipe", which had no Varlink counterpart. The manager Varlink server now enables SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT so clients may attach descriptors to their method calls, matching other fd-accepting Varlink services (mountfsd, networkd, vmspawn, ...). The indices are resolved with sd_varlink_peek_dup_fd() after the polkit authorization check and stored on the Service exactly like the D-Bus path (bus_set_transient_exec_context_fd()): the fds land in stdin_fd/stdout_fd/ stderr_fd and exec_context.stdio_as_fds is set, so the existing exec-invoke plumbing wires them to the spawned process unchanged. Unsuitable fds (bad index, wrong access mode) are rejected as InvalidParameter, other resolution failures propagate as raw errnos. Add coverage to TEST-74-AUX-UTILS.varlinkctl-unit.sh, passing regular files as the stdout/stderr fds and asserting the unit's output lands on them. --- src/core/varlink-unit.c | 116 +++++++++++++++++- src/core/varlink.c | 4 +- src/shared/varlink-io.systemd.Unit.c | 6 + .../TEST-74-AUX-UTILS.varlinkctl-unit.sh | 19 +++ 4 files changed, 141 insertions(+), 4 deletions(-) diff --git a/src/core/varlink-unit.c b/src/core/varlink-unit.c index c95dc8c45214c..cd6df5a5b844e 100644 --- a/src/core/varlink-unit.c +++ b/src/core/varlink-unit.c @@ -3,12 +3,14 @@ #include "sd-bus.h" #include "sd-json.h" +#include "async.h" #include "bitfield.h" #include "bus-polkit.h" #include "cgroup.h" #include "condition.h" #include "dbus-job.h" #include "execute.h" +#include "fd-util.h" #include "format-util.h" #include "install.h" #include "iovec-util.h" @@ -739,6 +741,14 @@ typedef struct TransientServiceParameters { TransientExecCommandItem *exec_start; size_t n_exec_start; int remain_after_exit; + /* Indices of stdio fds attached to the method call (SCM_RIGHTS), UINT_MAX if unset. Resolved into + * the owned fds below by transient_service_resolve_stdio_fds(). */ + unsigned stdin_fd_index; + unsigned stdout_fd_index; + unsigned stderr_fd_index; + int stdin_fd; + int stdout_fd; + int stderr_fd; } TransientServiceParameters; static void transient_service_parameters_done(TransientServiceParameters *p) { @@ -746,6 +756,9 @@ static void transient_service_parameters_done(TransientServiceParameters *p) { FOREACH_ARRAY(i, p->exec_start, p->n_exec_start) transient_exec_command_item_done(i); free(p->exec_start); + safe_close(p->stdin_fd); + safe_close(p->stdout_fd); + safe_close(p->stderr_fd); } static void transient_service_parameters_init(TransientServiceParameters *p) { @@ -753,6 +766,12 @@ static void transient_service_parameters_init(TransientServiceParameters *p) { *p = (TransientServiceParameters) { .type = _SERVICE_TYPE_INVALID, .remain_after_exit = -1, + .stdin_fd_index = UINT_MAX, + .stdout_fd_index = UINT_MAX, + .stderr_fd_index = UINT_MAX, + .stdin_fd = -EBADF, + .stdout_fd = -EBADF, + .stderr_fd = -EBADF, }; } @@ -938,9 +957,12 @@ static int dispatch_transient_exec_context(const char *name, sd_json_variant *va static int dispatch_transient_service(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { static const sd_json_dispatch_field service_dispatch[] = { - { "Type", SD_JSON_VARIANT_STRING, dispatch_service_type, offsetof(TransientServiceParameters, type), 0 }, - { "ExecStart", SD_JSON_VARIANT_ARRAY, dispatch_transient_exec_command, 0, 0 }, - { "RemainAfterExit", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(TransientServiceParameters, remain_after_exit), 0 }, + { "Type", SD_JSON_VARIANT_STRING, dispatch_service_type, offsetof(TransientServiceParameters, type), 0 }, + { "ExecStart", SD_JSON_VARIANT_ARRAY, dispatch_transient_exec_command, 0, 0 }, + { "RemainAfterExit", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(TransientServiceParameters, remain_after_exit), 0 }, + { "StandardInputFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(TransientServiceParameters, stdin_fd_index), 0 }, + { "StandardOutputFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(TransientServiceParameters, stdout_fd_index), 0 }, + { "StandardErrorFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(TransientServiceParameters, stderr_fd_index), 0 }, {} }; @@ -1435,6 +1457,66 @@ static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, Tran return 0; } +static int transient_service_resolve_one_stdio_fd( + sd_varlink *link, + unsigned idx, + int accmode, + int *ret_fd) { + + int r; + + assert(link); + assert(ret_fd); + + if (idx == UINT_MAX) /* not specified */ + return 0; + + /* Dup non-destructively off the connection, so that a re-dispatch of the method (e.g. after a + * deferred polkit authorization) can resolve the same index again. */ + _cleanup_close_ int fd = sd_varlink_peek_dup_fd(link, (size_t) idx); + if (fd < 0) + return fd; + + r = fd_vet_accmode(fd, accmode); + if (r < 0) + return r; + + *ret_fd = TAKE_FD(fd); + return 1; +} + +/* Resolve stdio fd indices to fds attached to the method call. Ownership of the resolved fds lands in + * the parameters and is released by transient_service_parameters_done(). */ +static int transient_service_resolve_stdio_fds(sd_varlink *link, TransientServiceParameters *sp, const char **reterr_field) { + int r; + + assert(link); + assert(sp); + + r = transient_service_resolve_one_stdio_fd(link, sp->stdin_fd_index, O_RDONLY, &sp->stdin_fd); + if (r < 0) { + if (reterr_field) + *reterr_field = "Service.StandardInputFileDescriptor"; + return r; + } + + r = transient_service_resolve_one_stdio_fd(link, sp->stdout_fd_index, O_WRONLY, &sp->stdout_fd); + if (r < 0) { + if (reterr_field) + *reterr_field = "Service.StandardOutputFileDescriptor"; + return r; + } + + r = transient_service_resolve_one_stdio_fd(link, sp->stderr_fd_index, O_WRONLY, &sp->stderr_fd); + if (r < 0) { + if (reterr_field) + *reterr_field = "Service.StandardErrorFileDescriptor"; + return r; + } + + return 0; +} + static int transient_service_apply_properties(Service *s, TransientServiceParameters *sp, const char **reterr_field) { Unit *u = UNIT(ASSERT_PTR(s)); int r; @@ -1446,6 +1528,25 @@ static int transient_service_apply_properties(Service *s, TransientServiceParame unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "Type", "Type=%s", service_type_to_string(sp->type)); } + /* Passed stdio fds, resolved earlier by transient_service_resolve_stdio_fds(): store them on the + * Service and flag the exec context. Not written as a text setting; fd serialization across + * reexec is handled by service_serialize(). */ + if (sp->stdin_fd >= 0) { + asynchronous_close(s->stdin_fd); + s->stdin_fd = TAKE_FD(sp->stdin_fd); + s->exec_context.stdio_as_fds = true; + } + if (sp->stdout_fd >= 0) { + asynchronous_close(s->stdout_fd); + s->stdout_fd = TAKE_FD(sp->stdout_fd); + s->exec_context.stdio_as_fds = true; + } + if (sp->stderr_fd >= 0) { + asynchronous_close(s->stderr_fd); + s->stderr_fd = TAKE_FD(sp->stderr_fd); + s->exec_context.stdio_as_fds = true; + } + if (sp->remain_after_exit >= 0) { s->remain_after_exit = sp->remain_after_exit; unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "RemainAfterExit", "RemainAfterExit=%s", yes_no(sp->remain_after_exit)); @@ -1578,6 +1679,15 @@ int vl_method_start_transient_unit(sd_varlink *link, sd_json_variant *parameters if (r <= 0) return r; + /* Resolve stdio fd indices against the fds attached to this method call before the unit exists, + * so a failure (bad index, wrong access mode) doesn't leave a half-configured transient unit. */ + bad_field = NULL; + r = transient_service_resolve_stdio_fds(link, &p.context.service, &bad_field); + if (IN_SET(r, -ENXIO, -EPROTOTYPE, -EBADFD, -EISDIR)) /* bad index or unsuitable fd */ + return sd_varlink_error_invalid_parameter_name(link, bad_field ?: "Service"); + if (r < 0) + return sd_varlink_error_errno(link, r); + r = manager_setup_transient_unit(manager, p.context.id, &u, &bus_error); if (r < 0) return varlink_reply_bus_error(link, r, &bus_error); diff --git a/src/core/varlink.c b/src/core/varlink.c index d30cb7cab3358..fa6a5e1565693 100644 --- a/src/core/varlink.c +++ b/src/core/varlink.c @@ -401,7 +401,9 @@ int manager_setup_varlink_server(Manager *m) { if (m->varlink_server) return 0; - sd_varlink_server_flags_t flags = SD_VARLINK_SERVER_INHERIT_USERDATA; + /* ALLOW_FD_PASSING_INPUT: allow clients to attach fds to method calls, used by + * io.systemd.Unit.StartTransient() to accept stdio fds. */ + sd_varlink_server_flags_t flags = SD_VARLINK_SERVER_INHERIT_USERDATA|SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT; if (MANAGER_IS_SYSTEM(m)) flags |= SD_VARLINK_SERVER_ACCOUNT_UID; diff --git a/src/shared/varlink-io.systemd.Unit.c b/src/shared/varlink-io.systemd.Unit.c index 5a8e7c98331ed..202b529e879f7 100644 --- a/src/shared/varlink-io.systemd.Unit.c +++ b/src/shared/varlink-io.systemd.Unit.c @@ -1417,6 +1417,12 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( ServiceContext, SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man/"PROJECT_VERSION_STR"/systemd.service.html#Type="), SD_VARLINK_DEFINE_FIELD_BY_TYPE(Type, ServiceType, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Index of a file descriptor passed along with the method call to connect to the service's standard input. Only settable at unit creation time via StartTransient(); never set in unit descriptions returned by List()."), + SD_VARLINK_DEFINE_FIELD(StandardInputFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Index of a file descriptor passed along with the method call to connect to the service's standard output. Only settable at unit creation time via StartTransient()."), + SD_VARLINK_DEFINE_FIELD(StandardOutputFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Index of a file descriptor passed along with the method call to connect to the service's standard error. Only settable at unit creation time via StartTransient()."), + SD_VARLINK_DEFINE_FIELD(StandardErrorFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man/"PROJECT_VERSION_STR"/systemd.service.html#ExitType="), SD_VARLINK_DEFINE_FIELD_BY_TYPE(ExitType, ServiceExitType, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man/"PROJECT_VERSION_STR"/systemd.service.html#Restart="), diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh index fc7d16b199c31..f20a7fd4257c5 100755 --- a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh @@ -264,6 +264,25 @@ test -n "$fragment" grep '^RootHash=/etc/hostname$' "$fragment" >/dev/null grep '^RootHashSignature=/etc/machine-id$' "$fragment" >/dev/null +# Service.Standard{Output,Error}FileDescriptor: connect passed fds (by push order) +# to the unit's stdout/stderr; regular files, so output can be checked after exit. +transient_out=$(mktemp) +transient_err=$(mktemp) +exec {transient_out_fd}>"$transient_out" +exec {transient_err_fd}>"$transient_err" +defer_transient_cleanup varlink-transient-fdpass.service +varlinkctl --push-fd="$transient_out_fd" --push-fd="$transient_err_fd" \ + call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-fdpass.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/sh","arguments":["/bin/sh","-c","echo to-stdout; echo to-stderr >&2"]}],"StandardOutputFileDescriptor":0,"StandardErrorFileDescriptor":1}}}' >/dev/null +# Close our copies so the unit holds the only remaining write ends. +exec {transient_out_fd}>&- +exec {transient_err_fd}>&- +timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-fdpass.service | grep inactive >/dev/null; do sleep 0.5; done' +systemctl show -P Result varlink-transient-fdpass.service | grep success >/dev/null +grep -x to-stdout "$transient_out" >/dev/null +grep -x to-stderr "$transient_err" >/dev/null +rm -f "$transient_out" "$transient_err" + # Error cases: verify specific varlink error types set +o pipefail varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ From 636b173035e9f24ed11f88542b0202328d759073 Mon Sep 17 00:00:00 2001 From: Syed Mohammed Nayyar Date: Sun, 5 Jul 2026 13:39:51 +0530 Subject: [PATCH 100/106] measure-smbios: bound type 1 length before zeroing wake-up type The structure length comes from the firmware SMBIOS table and the only bound before the fixed-offset wake_up_type write was an assert(), which is a no-op in release sd-boot builds. Whether the field is present is governed by the formatted-area length header->length, not the total size which also covers the trailing string set. A too-short record (an old SMBIOS 2.0 one, or a crafted 6-byte header) makes the offset-24 write land past the xmemdup() copy; a record with a short formatted area but long string set instead gets a stable string byte zeroed in the copy. Key the guard off header->length and the field offset, matching get_smbios_table(), and measure such records as-is. Zero only the part of the field that lies within the formatted area, and collapse the two measure_smbios_raw() call sites by selecting what to measure via a pointer. Signed-off-by: Syed Mohammed Nayyar --- src/boot/measure-smbios.c | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/boot/measure-smbios.c b/src/boot/measure-smbios.c index e29440eb11761..89da7d9e8f97e 100644 --- a/src/boot/measure-smbios.c +++ b/src/boot/measure-smbios.c @@ -41,14 +41,27 @@ static void measure_smbios_type1(const SmbiosHeader *header, size_t size, bool * /* The wake-up type field varies depending on how the machine was powered on (cold boot, resume * from sleep, AC restore, …), which would make the measurement non-reproducible. Hence measure a - * copy with that field zeroed out. */ - - assert(size >= sizeof(SmbiosTableType1)); - - _cleanup_free_ SmbiosTableType1 *copy = xmemdup(header, size); - copy->wake_up_type = 0; + * copy with that field zeroed out. + * + * Whether the field is present is governed by the formatted-area length header->length (as in + * get_smbios_table()), not the total size which also covers the trailing string set. If the + * formatted area is too short to reach it (e.g. a legacy SMBIOS 2.0 record), there's nothing to + * normalize, so measure the structure as-is. This length comes from the (untrusted) firmware + * SMBIOS table and assert() is a no-op in release sd-boot builds, so it must be checked here + * before the copy is written to at a fixed offset. */ + + _cleanup_free_ SmbiosTableType1 *copy = NULL; + const void *p = header; + + if (header->length > offsetof(SmbiosTableType1, wake_up_type)) { + copy = xmemdup(header, size); + memzero((uint8_t*) copy + offsetof(SmbiosTableType1, wake_up_type), + MIN(sizeof_field(SmbiosTableType1, wake_up_type), + header->length - offsetof(SmbiosTableType1, wake_up_type))); + p = copy; + } - measure_smbios_raw(copy, size, SMBIOS_TYPE1_EVENT_TAG_ID, u"smbios:type1", measured); + measure_smbios_raw(p, size, SMBIOS_TYPE1_EVENT_TAG_ID, u"smbios:type1", measured); } static bool measure_smbios_object(const SmbiosHeader *header, size_t size, void *userdata) { From 1b0330399827696f1818ba8515217e2fe5350656 Mon Sep 17 00:00:00 2001 From: Elliot Berman Date: Tue, 7 Jul 2026 16:28:10 -0700 Subject: [PATCH 101/106] bootctl: Fix prepend when installing systemd-boot for the first time In commit 38433a6d06ef ("bootctl: rework bootctl-install.c in preparation of varlinkification"), the `first` argument of install_boot_option() was reworked to use the new InstallContext struct/InstallOperation. `first` was intended to indicate if we were on the install path, so the check should be == INSTALL_NEW, not != INSTALL_NEW. Fixes: 38433a6d06ef ("bootctl: rework bootctl-install.c in preparation of varlinkification") --- src/bootctl/bootctl-install.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c index 381ee95223a48..3d8b0e65315ca 100644 --- a/src/bootctl/bootctl-install.c +++ b/src/bootctl/bootctl-install.c @@ -1339,7 +1339,7 @@ static int insert_into_order(InstallContext *c, uint16_t slot, uint16_t after_sl order = t; /* add us to the top or end of the list */ - if (c->operation != INSTALL_NEW) { + if (c->operation == INSTALL_NEW) { memmove(order + 1, order, n * sizeof(uint16_t)); order[0] = slot; } else From da9a330dfd41e91cbfa6a6def2abb3706e280aa4 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Wed, 8 Jul 2026 16:53:30 +0800 Subject: [PATCH 102/106] fsck: don't apply invalid mode or repair values fsck_mode_from_string() and fsck_repair_from_string() return -EINVAL on a bad value. Store the result in a local variable first, and only update the global state on success. Otherwise an invalid value is logged as ignored, but still leaves a negative enum value behind. In the fsck.repair case that makes fsck_repair_option_to_string() return NULL and truncates the fsck command line. Follow-up for a85428b1d325654dc7e1afbabf4b689bd31116f5 Follow-up for 059afcadfd89c6c302f20c9ac1a1c44592b716df Signed-off-by: dongshengyuan --- src/fsck/fsck.c | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/fsck/fsck.c b/src/fsck/fsck.c index 405e9c34fddc4..cc8bfea681bf7 100644 --- a/src/fsck/fsck.c +++ b/src/fsck/fsck.c @@ -108,18 +108,22 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat if (proc_cmdline_value_missing(key, value)) return 0; - arg_mode = fsck_mode_from_string(value); - if (arg_mode < 0) - log_warning_errno(arg_mode, "Invalid fsck.mode= parameter, ignoring: %s", value); + FSCKMode mode = fsck_mode_from_string(value); + if (mode < 0) + log_warning_errno(mode, "Invalid fsck.mode= parameter, ignoring: %s", value); + else + arg_mode = mode; } else if (streq(key, "fsck.repair")) { if (proc_cmdline_value_missing(key, value)) return 0; - arg_repair = fsck_repair_from_string(value); - if (arg_repair < 0) - log_warning_errno(arg_repair, "Invalid fsck.repair= parameter, ignoring: %s", value); + FSCKRepair repair = fsck_repair_from_string(value); + if (repair < 0) + log_warning_errno(repair, "Invalid fsck.repair= parameter, ignoring: %s", value); + else + arg_repair = repair; } else if (streq(key, "fastboot") && !value) @@ -139,9 +143,11 @@ static void parse_credentials(void) { if (r < 0) log_debug_errno(r, "Failed to read credential 'fsck.mode', ignoring: %m"); else { - arg_mode = fsck_mode_from_string(value); - if (arg_mode < 0) - log_warning_errno(arg_mode, "Invalid 'fsck.mode' credential, ignoring: %s", value); + FSCKMode mode = fsck_mode_from_string(value); + if (mode < 0) + log_warning_errno(mode, "Invalid 'fsck.mode' credential, ignoring: %s", value); + else + arg_mode = mode; } value = mfree(value); @@ -150,9 +156,11 @@ static void parse_credentials(void) { if (r < 0) log_debug_errno(r, "Failed to read credential 'fsck.repair', ignoring: %m"); else { - arg_repair = fsck_repair_from_string(value); - if (arg_repair < 0) - log_warning_errno(arg_repair, "Invalid 'fsck.repair' credential, ignoring: %s", value); + FSCKRepair repair = fsck_repair_from_string(value); + if (repair < 0) + log_warning_errno(repair, "Invalid 'fsck.repair' credential, ignoring: %s", value); + else + arg_repair = repair; } } From 34a222334432c1bafc269b600a8298697a8d7712 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Wed, 8 Jul 2026 14:03:15 +0800 Subject: [PATCH 103/106] dns-rr: fix SOA JSON fields Signed-off-by: dongshengyuan --- src/resolve/test-dns-rr.c | 24 ++++++++++++++++++++++++ src/shared/dns-rr.c | 3 ++- src/shared/varlink-io.systemd.Resolve.c | 1 + 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/resolve/test-dns-rr.c b/src/resolve/test-dns-rr.c index 2ded6b0ab96f9..d77ae0639d824 100644 --- a/src/resolve/test-dns-rr.c +++ b/src/resolve/test-dns-rr.c @@ -2240,6 +2240,30 @@ TEST(dns_resource_record_to_string_soa) { ASSERT_STREQ(str, "www.example.com IN SOA ns0.example.com ns0.example.com 1111111111 86400 7200 4000000 3600"); } +TEST(dns_resource_record_to_json_soa) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL; + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_SOA, "www.example.com"); + ASSERT_NOT_NULL(rr); + rr->soa.mname = strdup("ns0.example.com"); + rr->soa.rname = strdup("hostmaster.example.com"); + rr->soa.serial = 1111111111; + rr->soa.refresh = 86400; + rr->soa.retry = 7200; + rr->soa.expire = 4000000; + rr->soa.minimum = 3600; + + ASSERT_OK(dns_resource_record_to_json(rr, &j)); + ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(j, "mname")), "ns0.example.com"); + ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(j, "rname")), "hostmaster.example.com"); + ASSERT_EQ(sd_json_variant_unsigned(sd_json_variant_by_key(j, "serial")), UINT64_C(1111111111)); + ASSERT_EQ(sd_json_variant_unsigned(sd_json_variant_by_key(j, "refresh")), UINT64_C(86400)); + ASSERT_EQ(sd_json_variant_unsigned(sd_json_variant_by_key(j, "retry")), UINT64_C(7200)); + ASSERT_EQ(sd_json_variant_unsigned(sd_json_variant_by_key(j, "expire")), UINT64_C(4000000)); + ASSERT_EQ(sd_json_variant_unsigned(sd_json_variant_by_key(j, "minimum")), UINT64_C(3600)); +} + TEST(dns_resource_record_to_string_ptr) { _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; const char *str; diff --git a/src/shared/dns-rr.c b/src/shared/dns-rr.c index eb91910e835f8..d594166290643 100644 --- a/src/shared/dns-rr.c +++ b/src/shared/dns-rr.c @@ -2374,7 +2374,8 @@ int dns_resource_record_to_json(DnsResourceRecord *rr, sd_json_variant **ret) { SD_JSON_BUILD_PAIR_STRING("rname", rr->soa.rname), SD_JSON_BUILD_PAIR_UNSIGNED("serial", rr->soa.serial), SD_JSON_BUILD_PAIR_UNSIGNED("refresh", rr->soa.refresh), - SD_JSON_BUILD_PAIR_UNSIGNED("expire", rr->soa.retry), + SD_JSON_BUILD_PAIR_UNSIGNED("retry", rr->soa.retry), + SD_JSON_BUILD_PAIR_UNSIGNED("expire", rr->soa.expire), SD_JSON_BUILD_PAIR_UNSIGNED("minimum", rr->soa.minimum)); case DNS_TYPE_MX: diff --git a/src/shared/varlink-io.systemd.Resolve.c b/src/shared/varlink-io.systemd.Resolve.c index 6c4ef094ca7bc..a5fbae4a977f3 100644 --- a/src/shared/varlink-io.systemd.Resolve.c +++ b/src/shared/varlink-io.systemd.Resolve.c @@ -67,6 +67,7 @@ SD_VARLINK_DEFINE_STRUCT_TYPE( SD_VARLINK_DEFINE_FIELD(rname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(serial, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(refresh, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_DEFINE_FIELD(retry, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(expire, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(minimum, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(exchange, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), From 0168b13fb3000f19e6574bf9593aa81348ed26f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C6=B0=C6=A1ng=20Vi=E1=BB=87t=20Ho=C3=A0ng?= Date: Tue, 7 Jul 2026 13:38:13 +0700 Subject: [PATCH 104/106] creds-util: log when we remove a secret from a different machine --- src/shared/creds-util.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index e1bf71bc3794c..eaee54ee57abc 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -610,6 +610,8 @@ int get_credential_host_secret(CredentialSecretFlags flags, struct iovec *ret) { /* Hmm, this secret is from somewhere else. Let's delete the file. Let's first acquire a lock * to ensure we are the only ones accessing the file while we delete it. */ + log_notice("'%s/%s' comes from a different machine ID, deleting.", dirname, filename); + if (flock(fd, LOCK_EX) < 0) return log_debug_errno(errno, "Failed to flock %s/%s: %m", dirname, filename); From 9ae50c78ccf7d36258a6779f34055248e9343efa Mon Sep 17 00:00:00 2001 From: Frantisek Sumsal Date: Tue, 7 Jul 2026 17:22:49 +0200 Subject: [PATCH 105/106] sd-event: use CLOCK_BOOTTIME for rate limits When a rate limit is armed, for example via StartLimitInterval=, and the machine is suspended, the rate limit's "clock" is suspended as well, since the elapse checks use CLOCK_MONOTONIC. This then causes unexpected situations where a rate limit armed via StartLimitInterval=1h, followed by a 10 hour suspend, would elapse after 11 hours total. Let's avoid this by switching the rate limits to CLOCK_BOOTTIME, which works the same as CLOCK_MONOTONIC, but accounts for the time spent in suspend as well. There's one slight concern when it comes to upgrade path - the old "begin" value of the rate limit is stored as CLOCK_MONOTONIC, but after upgrading systemd and serializing/deserializing the state it will be suddenly compared against now(CLOCK_BOOTTIME), which might cause some rate limits to elapse "prematurely". But this is just a one-time thing, after which the rate limit timers should re-assess themselves. Resolves: #42912 --- src/basic/ratelimit.c | 4 +-- src/core/manager.c | 2 +- src/libsystemd/sd-event/sd-event.c | 48 ++++++++++++++++----------- src/nsresourced/nsresourced-manager.c | 2 +- src/userdb/userdbd-manager.c | 2 +- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/basic/ratelimit.c b/src/basic/ratelimit.c index 48ad08f3b6709..b4ef5470b052f 100644 --- a/src/basic/ratelimit.c +++ b/src/basic/ratelimit.c @@ -14,7 +14,7 @@ bool ratelimit_below(RateLimit *rl) { if (!ratelimit_configured(rl)) return true; - ts = now(CLOCK_MONOTONIC); + ts = now(CLOCK_BOOTTIME); if (rl->begin <= 0 || usec_sub_unsigned(ts, rl->begin) > rl->interval) { @@ -54,5 +54,5 @@ usec_t ratelimit_left(const RateLimit *rl) { if (rl->begin == 0) return 0; - return usec_sub_unsigned(ratelimit_end(rl), now(CLOCK_MONOTONIC)); + return usec_sub_unsigned(ratelimit_end(rl), now(CLOCK_BOOTTIME)); } diff --git a/src/core/manager.c b/src/core/manager.c index ae44f87b2a863..27ca84c4e9e75 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -1517,7 +1517,7 @@ static int manager_ratelimit_check_and_queue(Unit *u) { r = sd_event_add_time( u->manager->event, &u->auto_start_stop_event_source, - CLOCK_MONOTONIC, + CLOCK_BOOTTIME, ratelimit_end(&u->auto_start_stop_ratelimit), 0, manager_ratelimit_requeue, diff --git a/src/libsystemd/sd-event/sd-event.c b/src/libsystemd/sd-event/sd-event.c index 4935ae1b4aac2..590ac06b8e38b 100644 --- a/src/libsystemd/sd-event/sd-event.c +++ b/src/libsystemd/sd-event/sd-event.c @@ -900,7 +900,7 @@ static void event_source_time_prioq_reshuffle(sd_event_source *s) { * properly again. */ if (s->ratelimited) - d = &s->event->monotonic; + d = &s->event->boottime; else if (EVENT_SOURCE_IS_TIME(s->type)) assert_se(d = event_get_clock_data(s->event, s->type)); else @@ -950,7 +950,7 @@ static void source_disconnect(sd_event_source *s) { case SOURCE_TIME_BOOTTIME_ALARM: /* Only remove this event source from the time event source here if it is not ratelimited. If * it is ratelimited, we'll remove it below, separately. Why? Because the clock used might - * differ: ratelimiting always uses CLOCK_MONOTONIC, but timer events might use any clock */ + * differ: ratelimiting always uses CLOCK_BOOTTIME, but timer events might use any clock */ if (!s->ratelimited) { struct clock_data *d; @@ -1070,7 +1070,7 @@ static void source_disconnect(sd_event_source *s) { prioq_remove(s->event->prepare, s, &s->prepare_index); if (s->ratelimited) - event_source_time_prioq_remove(s, &s->event->monotonic); + event_source_time_prioq_remove(s, &s->event->boottime); event = TAKE_PTR(s->event); LIST_REMOVE(sources, event->sources, s); @@ -3397,32 +3397,32 @@ static int event_source_enter_ratelimited(sd_event_source *s) { assert(s); - /* When an event source becomes ratelimited, we place it in the CLOCK_MONOTONIC priority queue, with + /* When an event source becomes ratelimited, we place it in the CLOCK_BOOTTIME priority queue, with * the end of the rate limit time window, much as if it was a timer event source. */ if (s->ratelimited) return 0; /* Already ratelimited, this is a NOP hence */ - /* Make sure we can install a CLOCK_MONOTONIC event further down. */ - r = setup_clock_data(s->event, &s->event->monotonic, CLOCK_MONOTONIC); + /* Make sure we can install a CLOCK_BOOTTIME event further down. */ + r = setup_clock_data(s->event, &s->event->boottime, CLOCK_BOOTTIME); if (r < 0) return r; /* Timer event sources are already using the earliest/latest queues for the timer scheduling. Let's * first remove them from the prioq appropriate for their own clock, so that we can use the prioq - * fields of the event source then for adding it to the CLOCK_MONOTONIC prioq instead. */ + * fields of the event source then for adding it to the CLOCK_BOOTTIME prioq instead. */ if (EVENT_SOURCE_IS_TIME(s->type)) event_source_time_prioq_remove(s, event_get_clock_data(s->event, s->type)); - /* Now, let's add the event source to the monotonic clock instead */ - r = event_source_time_prioq_put(s, &s->event->monotonic); + /* Now, let's add the event source to the boottime clock instead */ + r = event_source_time_prioq_put(s, &s->event->boottime); if (r < 0) goto fail; /* And let's take the event source officially offline */ r = event_source_offline(s, s->enabled, /* ratelimited= */ true); if (r < 0) { - event_source_time_prioq_remove(s, &s->event->monotonic); + event_source_time_prioq_remove(s, &s->event->boottime); goto fail; } @@ -3448,8 +3448,8 @@ static int event_source_leave_ratelimit(sd_event_source *s, bool run_callback) { if (!s->ratelimited) return 0; - /* Let's take the event source out of the monotonic prioq first. */ - event_source_time_prioq_remove(s, &s->event->monotonic); + /* Let's take the event source out of the boottime prioq first. */ + event_source_time_prioq_remove(s, &s->event->boottime); /* Let's then add the event source to its native clock prioq again — if this is a timer event source */ if (EVENT_SOURCE_IS_TIME(s->type)) { @@ -3501,7 +3501,7 @@ static int event_source_leave_ratelimit(sd_event_source *s, bool run_callback) { fail: /* Do something somewhat reasonable when we cannot move an event sources out of ratelimited mode: * simply put it back in it, maybe we can then process it more successfully next iteration. */ - assert_se(event_source_time_prioq_put(s, &s->event->monotonic) >= 0); + assert_se(event_source_time_prioq_put(s, &s->event->boottime) >= 0); return r; } @@ -4781,6 +4781,7 @@ static int process_epoll(sd_event *e, usec_t timeout, int64_t threshold, int64_t } _public_ int sd_event_wait(sd_event *e, uint64_t timeout) { + bool ratelimit_expired = false; int r; assert_return(e, -EINVAL); @@ -4845,6 +4846,8 @@ _public_ int sd_event_wait(sd_event *e, uint64_t timeout) { r = process_timer(e, e->timestamp.boottime, &e->boottime); if (r < 0) goto finish; + else if (r == 1) + ratelimit_expired = true; r = process_timer(e, e->timestamp.realtime, &e->realtime_alarm); if (r < 0) @@ -4857,14 +4860,19 @@ _public_ int sd_event_wait(sd_event *e, uint64_t timeout) { r = process_timer(e, e->timestamp.monotonic, &e->monotonic); if (r < 0) goto finish; - else if (r == 1) { - /* Ratelimit expiry callback was called. Let's postpone processing pending sources and - * put loop in the initial state in order to evaluate (in the next iteration) also sources - * there were potentially re-enabled by the callback. + + if (ratelimit_expired) { + /* Ratelimit expiry callback was called. Let's postpone processing pending sources and put + * the loop in the initial state in order to evaluate (in the next iteration) also sources + * that were potentially re-enabled by the callback. + * + * Wondering why we treat only the CLOCK_BOOTTIME invocation of process_timer() differently? + * Once event source is ratelimited we essentially transform it into a CLOCK_BOOTTIME timer + * hence ratelimit expiry callback is never called for any other timer type. * - * Wondering why we treat only this invocation of process_timer() differently? Once event - * source is ratelimited we essentially transform it into CLOCK_MONOTONIC timer hence - * ratelimit expiry callback is never called for any other timer type. */ + * Note that we don't short-circuit earlier because all timer prioqs must be fully processed + * as their timerfds may have already been flushed by process_epoll(), so skipping them would + * leave those sources unprocessed and the timerfds un-rearmed. */ r = 0; goto finish; } diff --git a/src/nsresourced/nsresourced-manager.c b/src/nsresourced/nsresourced-manager.c index 7d5f27bbda89d..4e671de833197 100644 --- a/src/nsresourced/nsresourced-manager.c +++ b/src/nsresourced/nsresourced-manager.c @@ -291,7 +291,7 @@ static int start_workers(Manager *m, bool explicit_request) { r = sd_event_add_time( m->event, &m->deferred_start_worker_event_source, - CLOCK_MONOTONIC, + CLOCK_BOOTTIME, ratelimit_end(&m->worker_ratelimit), /* accuracy= */ 0, on_deferred_start_worker, diff --git a/src/userdb/userdbd-manager.c b/src/userdb/userdbd-manager.c index cf9f2f5c6b8c3..c51ae513f4eb9 100644 --- a/src/userdb/userdbd-manager.c +++ b/src/userdb/userdbd-manager.c @@ -259,7 +259,7 @@ static int start_workers(Manager *m, bool explicit_request) { r = sd_event_add_time( m->event, &m->deferred_start_worker_event_source, - CLOCK_MONOTONIC, + CLOCK_BOOTTIME, ratelimit_end(&m->worker_ratelimit), /* accuracy= */ 0, on_deferred_start_worker, From 411ad593076cca0d379f365263e924187e00769f Mon Sep 17 00:00:00 2001 From: Simran Singh Date: Wed, 10 Dec 2025 06:14:53 +0530 Subject: [PATCH 106/106] 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 | 76 ++++ meson.build | 2 + src/clonesetup/clonesetup-generator.c | 226 ++++++++++++ src/clonesetup/clonesetup-ioctl.c | 276 ++++++++++++++ src/clonesetup/clonesetup-ioctl.h | 16 + src/clonesetup/clonesetup-util.c | 37 ++ src/clonesetup/clonesetup-util.h | 6 + src/clonesetup/clonesetup.c | 192 ++++++++++ src/clonesetup/meson.build | 19 + .../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 + 18 files changed, 1400 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-util.c create mode 100644 src/clonesetup/clonesetup-util.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..aa202e57fa75b --- /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 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. Defaults to 4K. For background, see + dm-clone kernel documentation. + + + + + + + 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 8 KiB: + mydevice /dev/sdb /dev/sdc /dev/sdd region-size=8K + + + + + 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..eace5a701f03f --- /dev/null +++ b/man/systemd-clonesetup.xml @@ -0,0 +1,76 @@ + + + + + + + + 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=BYTES, where + BYTES is measured in bytes (standard suffixes like + K, M, G are supported). + + + + See Also + + systemd1 + clonetab5 + systemd-clonesetup-generator8 + systemd.generator7 + dmsetup8 + + + + diff --git a/meson.build b/meson.build index 81bf99f415312..b92010e6d7bcb 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..77b1c96d0305b --- /dev/null +++ b/src/clonesetup/clonesetup-generator.c @@ -0,0 +1,226 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "alloc-util.h" +#include "clonesetup-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 "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 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_warning("Failed to parse %s:%u, ignoring.", fname, clone_line); + continue; + } + + log_debug("Processing %s:%u", fname, clone_line); + r = validate_fields(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..73c52e6e99770 --- /dev/null +++ b/src/clonesetup/clonesetup-ioctl.c @@ -0,0 +1,276 @@ +/* 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, temp_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 */ + 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; +} + +/* 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) { + 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; +} + +/* First dm ioctl needed to create a device. */ +static int dm_clone_create(const char *name) { + int r; + assert(name); + + struct dm_ioctl 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. */ +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 *) 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_bytes) { + + _cleanup_free_ char *target_params = NULL; + uint64_t src_dev_size_sectors, src_dev_size, region_size_sectors; + 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; + + 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 + * 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_sectors) < 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..ca3a373fc63a4 --- /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_bytes); + +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-util.c b/src/clonesetup/clonesetup-util.c new file mode 100644 index 0000000000000..da9ea634d4c17 --- /dev/null +++ b/src/clonesetup/clonesetup-util.c @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "clonesetup-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; +} + +int validate_fields(const char *name, const char *src, const char *dst, + const char *meta, const char *options) { + int r; + + if (!filename_is_valid(name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid clone name '%s'.", name); + + 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'.", options); + + return 0; +} diff --git a/src/clonesetup/clonesetup-util.h b/src/clonesetup/clonesetup-util.h new file mode 100644 index 0000000000000..ef64edb17aca3 --- /dev/null +++ b/src/clonesetup/clonesetup-util.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int validate_dev_path(const char *what, const char *path); +int validate_fields(const char *name, const char *src, const char *dst, + const char *meta, const char *options); diff --git a/src/clonesetup/clonesetup.c b/src/clonesetup/clonesetup.c new file mode 100644 index 0000000000000..debd10590a1dc --- /dev/null +++ b/src/clonesetup/clonesetup.c @@ -0,0 +1,192 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + +#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" +#include "log.h" +#include "main-func.h" +#include "options.h" +#include "parse-util.h" +#include "string-util.h" +#include "strv.h" /* strv_skip */ +#include "verbs.h" + +/* 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_bytes) { + uint64_t region_size_bytes = CLONE_REGION_SIZE_DEFAULT_BYTES; + + assert(ret_region_size_bytes); + + 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; + + /* treat - as empty — common placeholders for "no options" */ + if (streq(word, "-")) + continue; + + if ((val = startswith(word, "region-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_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_bytes = r_size_bytes; + } else + /* currently only region-size is supported */ + log_warning("Unknown clone option '%s', ignoring.", word); + } + *ret_region_size_bytes = region_size_bytes; + 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) { + + int r; + + assert(clone_name); + assert(source_dev); + assert(dest_dev); + assert(metadata_dev); + + 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_bytes); + 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; +} + +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 == -EBUSY) + return dm_clone_remove_device_deferred(name); + 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..044f69b813867 --- /dev/null +++ b/src/clonesetup/meson.build @@ -0,0 +1,19 @@ +# 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, + 'export' : files('clonesetup-util.c'), + }, + generator_template + { + 'name' : 'systemd-clonesetup-generator', + 'sources' : files('clonesetup-generator.c'), + 'import' : ['systemd-clonesetup'], + }, +] 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..537c40b9720f2 --- /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 <