From 4347c3bcceaf37b3a289e4a7c4b50941f980d66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 24 Mar 2026 23:30:13 +0100 Subject: [PATCH 01/17] report: use the new option and verb macros --- src/report/report.c | 148 ++++++++++++++++++-------------------------- 1 file changed, 60 insertions(+), 88 deletions(-) diff --git a/src/report/report.c b/src/report/report.c index 74366cffdf..96ff28dccd 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -1,7 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include - #include "sd-event.h" #include "sd-varlink.h" @@ -13,6 +11,7 @@ #include "format-table.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "parse-argument.h" #include "path-lookup.h" #include "pretty-print.h" @@ -708,6 +707,10 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) { return m > 0; } +VERB_FULL(verb_metrics, "metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_LIST_METRICS, + "Acquire list of metrics and their values"); +VERB_FULL(verb_metrics, "describe-metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS, + "Describe available metrics"); static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) { Action action = data; int r; @@ -788,6 +791,10 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) return 0; } +VERB_FULL(verb_facts, "facts", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_LIST_FACTS, + "Acquire list of facts and their values"); +VERB_FULL(verb_facts, "describe-facts", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_FACTS, + "Describe available facts"); static int verb_facts(int argc, char *argv[], uintptr_t data, void *userdata) { Action action = data; int r; @@ -868,6 +875,7 @@ static int verb_facts(int argc, char *argv[], uintptr_t data, void *userdata) { return 0; } +VERB_NOARG(verb_list_sources, "list-sources", "Show list of known metrics sources"); static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *userdata) { int r; @@ -925,143 +933,107 @@ static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *user static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *verbs = NULL, *options = NULL; int r; r = terminal_urlify_man("systemd-report", "1", &link); if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] COMMAND ...\n" - "\n%5$sAcquire metrics and facts from local sources.%6$s\n" - "\n%3$sCommands:%4$s\n" - " metrics [MATCH...] Acquire list of metrics and their values\n" - " describe-metrics [MATCH...]\n" - " Describe available metrics\n" - " facts [MATCH...] Acquire list of facts and their values\n" - " describe-facts [MATCH...]\n" - " Describe available facts\n" - " list-sources Show list of known metrics sources\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --no-pager Do not pipe output into a pager\n" - " --no-legend Do not show the headers and footers\n" - " --user Connect to user service manager\n" - " --system Connect to system service manager (default)\n" - " --json=pretty|short\n" - " Configure JSON output\n" - " -j Equivalent to --json=pretty (on TTY) or --json=short\n" - " (otherwise)\n" - "\nSee the %2$s for details.\n", - program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), - ansi_highlight(), - ansi_normal()); + r = verbs_get_help_table(&verbs); + if (r < 0) + return r; + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + (void) table_sync_column_widths(0, options, verbs); + + printf("%s [OPTIONS...] COMMAND ...\n" + "\n%sAcquire metrics and facts from local sources.%s\n" + "\n%sCommands:%s\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + ansi_underline(), + ansi_normal()); + r = table_print_or_warn(verbs); + if (r < 0) + return r; + + printf("\n%sOptions:%s\n", + ansi_underline(), + ansi_normal()); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } -static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) { - return help(); -} +VERB_COMMON_HELP_HIDDEN(help); -static int parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_NO_PAGER, - ARG_NO_LEGEND, - ARG_USER, - ARG_SYSTEM, - ARG_JSON, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, - { "user", no_argument, NULL, ARG_USER }, - { "system", no_argument, NULL, ARG_SYSTEM }, - { "json", required_argument, NULL, ARG_JSON }, - {} - }; - - int c, r; +static int parse_argv(int argc, char *argv[], char ***ret_args) { + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hj", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'h': + OPTION_COMMON_HELP: return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - case ARG_NO_PAGER: + OPTION_COMMON_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; - case ARG_NO_LEGEND: + OPTION_COMMON_NO_LEGEND: arg_legend = false; break; - case ARG_USER: + OPTION_LONG("user", NULL, "Connect to user service manager"): arg_runtime_scope = RUNTIME_SCOPE_USER; break; - case ARG_SYSTEM: + OPTION_LONG("system", NULL, "Connect to system service manager (default)"): arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; break; - case ARG_JSON: - r = parse_json_argument(optarg, &arg_json_format_flags); + OPTION_COMMON_JSON: + r = parse_json_argument(arg, &arg_json_format_flags); if (r <= 0) return r; - break; - case 'j': + OPTION_COMMON_LOWERCASE_J: arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } + *ret_args = option_parser_get_args(&state); return 1; } -static int report_main(int argc, char *argv[]) { - static const Verb verbs[] = { - { "help", VERB_ANY, 1, 0, verb_help }, - { "metrics", VERB_ANY, VERB_ANY, 0, verb_metrics, ACTION_LIST_METRICS }, - { "describe-metrics", VERB_ANY, VERB_ANY, 0, verb_metrics, ACTION_DESCRIBE_METRICS }, - { "facts", VERB_ANY, VERB_ANY, 0, verb_facts, ACTION_LIST_FACTS }, - { "describe-facts", VERB_ANY, VERB_ANY, 0, verb_facts, ACTION_DESCRIBE_FACTS }, - { "list-sources", VERB_ANY, 1, 0, verb_list_sources }, - {} - }; - - return dispatch_verb(argc, argv, verbs, NULL); -} - static int run(int argc, char *argv[]) { + char **args = NULL; int r; log_setup(); - r = parse_argv(argc, argv); + r = parse_argv(argc, argv, &args); if (r <= 0) return r; - return report_main(argc, argv); + return dispatch_verb_with_args(args, NULL); } DEFINE_MAIN_FUNCTION(run); From 0ba59c3d327eff3108904f06de7e55675ac15e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 9 Feb 2026 12:02:03 +0100 Subject: [PATCH 02/17] journal-upload: modernize macro wrapping curl_easy_setopt We cannot use a function, because the type is unknown and we want to stringify the option name, but we can use a block macro to make this a bit nicer, with normal code structure in the caller. --- src/journal-remote/journal-upload.c | 117 +++++++++++++--------------- 1 file changed, 55 insertions(+), 62 deletions(-) diff --git a/src/journal-remote/journal-upload.c b/src/journal-remote/journal-upload.c index e6cb5dabc2..e2038dd4b6 100644 --- a/src/journal-remote/journal-upload.c +++ b/src/journal-remote/journal-upload.c @@ -81,16 +81,14 @@ static void close_fd_input(Uploader *u); #define STATE_FILE "/var/lib/systemd/journal-upload/state" -#define easy_setopt(curl, opt, value, level, cmd) \ - do { \ - code = curl_easy_setopt(curl, opt, value); \ - if (code) { \ - log_full(level, \ - "curl_easy_setopt " #opt " failed: %s", \ - curl_easy_strerror(code)); \ - cmd; \ - } \ - } while (0) +#define easy_setopt(curl, log_level, opt, value) ({ \ + CURLcode code = curl_easy_setopt(ASSERT_PTR(curl), opt, value); \ + if (code) \ + log_full(log_level, \ + "curl_easy_setopt %s failed: %s", \ + #opt, curl_easy_strerror(code)); \ + !code; \ +}) DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURL*, curl_easy_cleanup, NULL); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct curl_slist*, curl_slist_free_all, NULL); @@ -194,8 +192,6 @@ int start_upload(Uploader *u, size_t nmemb, void *userdata), void *data) { - CURLcode code; - assert(u); assert(input_callback); @@ -262,64 +258,63 @@ int start_upload(Uploader *u, "Call to curl_easy_init failed."); /* If configured, set a timeout for the curl operation. */ - if (arg_network_timeout_usec != USEC_INFINITY) - easy_setopt(curl, CURLOPT_TIMEOUT, - (long) DIV_ROUND_UP(arg_network_timeout_usec, USEC_PER_SEC), - LOG_ERR, return -EXFULL); + if (arg_network_timeout_usec != USEC_INFINITY && + !easy_setopt(curl, LOG_ERR, CURLOPT_TIMEOUT, + (long) DIV_ROUND_UP(arg_network_timeout_usec, USEC_PER_SEC))) + return -EXFULL; /* tell it to POST to the URL */ - easy_setopt(curl, CURLOPT_POST, 1L, - LOG_ERR, return -EXFULL); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_POST, 1L)) + return -EXFULL; - easy_setopt(curl, CURLOPT_ERRORBUFFER, u->error, - LOG_ERR, return -EXFULL); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_ERRORBUFFER, u->error)) + return -EXFULL; /* set where to write to */ - easy_setopt(curl, CURLOPT_WRITEFUNCTION, output_callback, - LOG_ERR, return -EXFULL); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEFUNCTION, output_callback)) + return -EXFULL; - easy_setopt(curl, CURLOPT_WRITEDATA, data, - LOG_ERR, return -EXFULL); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEDATA, data)) + return -EXFULL; /* set where to read from */ - easy_setopt(curl, CURLOPT_READFUNCTION, input_callback, - LOG_ERR, return -EXFULL); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_READFUNCTION, input_callback)) + return -EXFULL; - easy_setopt(curl, CURLOPT_READDATA, data, - LOG_ERR, return -EXFULL); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_READDATA, data)) + return -EXFULL; /* use our special own mime type and chunked transfer */ - easy_setopt(curl, CURLOPT_HTTPHEADER, u->header, - LOG_ERR, return -EXFULL); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_HTTPHEADER, u->header)) + return -EXFULL; if (DEBUG_LOGGING) /* enable verbose for easier tracing */ - easy_setopt(curl, CURLOPT_VERBOSE, 1L, LOG_WARNING, ); + (void) easy_setopt(curl, LOG_WARNING, CURLOPT_VERBOSE, 1L); - easy_setopt(curl, CURLOPT_USERAGENT, - "systemd-journal-upload " GIT_VERSION, - LOG_WARNING, ); + (void) easy_setopt(curl, LOG_WARNING, + CURLOPT_USERAGENT, "systemd-journal-upload " GIT_VERSION); if (!streq_ptr(arg_key, "-") && (arg_key || startswith(u->url, "https://"))) { - easy_setopt(curl, CURLOPT_SSLKEY, arg_key ?: PRIV_KEY_FILE, - LOG_ERR, return -EXFULL); - easy_setopt(curl, CURLOPT_SSLCERT, arg_cert ?: CERT_FILE, - LOG_ERR, return -EXFULL); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLKEY, arg_key ?: PRIV_KEY_FILE)) + return -EXFULL; + if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLCERT, arg_cert ?: CERT_FILE)) + return -EXFULL; } if (STRPTR_IN_SET(arg_trust, "-", "all")) { log_info("Server certificate verification disabled."); - easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L, - LOG_ERR, return -EUCLEAN); - easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L, - LOG_ERR, return -EUCLEAN); - } else if (arg_trust || startswith(u->url, "https://")) - easy_setopt(curl, CURLOPT_CAINFO, arg_trust ?: TRUST_FILE, - LOG_ERR, return -EXFULL); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSL_VERIFYPEER, 0L)) + return -EUCLEAN; + if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSL_VERIFYHOST, 0L)) + return -EUCLEAN; + } else if (arg_trust || startswith(u->url, "https://")) { + if (!easy_setopt(curl, LOG_ERR, CURLOPT_CAINFO, arg_trust ?: TRUST_FILE)) + return -EXFULL; + } if (arg_key || arg_trust) - easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1, - LOG_WARNING, ); + (void) easy_setopt(curl, LOG_WARNING, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1); u->easy = TAKE_PTR(curl); } else { @@ -330,11 +325,8 @@ int start_upload(Uploader *u, } /* upload to this place */ - code = curl_easy_setopt(u->easy, CURLOPT_URL, u->url); - if (code) - return log_error_errno(SYNTHETIC_ERRNO(EXFULL), - "curl_easy_setopt CURLOPT_URL failed: %s", - curl_easy_strerror(code)); + if (!easy_setopt(u->easy, LOG_ERR, CURLOPT_URL, u->url)) + return -EXFULL; u->uploading = true; @@ -567,15 +559,15 @@ static int update_content_encoding_header(Uploader *u, const CompressionConfig * break; } - if (update_header) { - CURLcode code; - easy_setopt(u->easy, CURLOPT_HTTPHEADER, u->header, LOG_WARNING, return -EXFULL); - } + if (update_header && + !easy_setopt(u->easy, LOG_WARNING, CURLOPT_HTTPHEADER, u->header)) + return -EXFULL; u->compression = cc; if (cc) - log_debug("Using compression algorithm %s with compression level %i.", compression_to_string(cc->algorithm), cc->level); + log_debug("Using compression algorithm %s with compression level %i.", + compression_to_string(cc->algorithm), cc->level); else log_debug("Disabled compression algorithm."); return 0; @@ -644,12 +636,13 @@ static int perform_upload(Uploader *u) { code = curl_easy_perform(u->easy); if (code) { if (u->error[0]) - log_error("Upload to %s failed: %.*s", - u->url, (int) sizeof(u->error), u->error); + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Upload to %s failed: %.*s", + u->url, (int) sizeof(u->error), u->error); else - log_error("Upload to %s failed: %s", - u->url, curl_easy_strerror(code)); - return -EIO; + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Upload to %s failed: %s", + u->url, curl_easy_strerror(code)); } code = curl_easy_getinfo(u->easy, CURLINFO_RESPONSE_CODE, &status); From cdc8aadaed7bf0bb8721a0eda5817e5c88a1b48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 31 Mar 2026 12:40:47 +0200 Subject: [PATCH 03/17] shared: move src/import/curl-util.h to src/shared/ Move more common definitions in the header file instead of repeating them in bunch of places. src/import/curl-util.[ch] is renamed so that it's shared more naturally with other components. --- src/imds/imdsd.c | 3 +-- src/imds/meson.build | 4 ++-- src/import/meson.build | 4 +--- src/journal-remote/journal-upload.c | 13 +------------ src/{import => shared}/curl-util.c | 0 src/{import => shared}/curl-util.h | 17 +++++++++++++---- src/shared/meson.build | 3 +++ 7 files changed, 21 insertions(+), 23 deletions(-) rename src/{import => shared}/curl-util.c (100%) rename src/{import => shared}/curl-util.h (69%) diff --git a/src/imds/imdsd.c b/src/imds/imdsd.c index c0ab089830..7831d1c68c 100644 --- a/src/imds/imdsd.c +++ b/src/imds/imdsd.c @@ -18,6 +18,7 @@ #include "chase.h" #include "copy.h" #include "creds-util.h" +#include "curl-util.h" #include "device-private.h" #include "dns-rr.h" #include "errno-util.h" @@ -53,8 +54,6 @@ #include "web-util.h" #include "xattr-util.h" -#include "../import/curl-util.h" - /* This implements a client to the AWS' and Azure's "Instance Metadata Service", as well as GCP's "VM * Metadata", i.e.: * diff --git a/src/imds/meson.build b/src/imds/meson.build index f9fa9b5f0f..cb919fec61 100644 --- a/src/imds/meson.build +++ b/src/imds/meson.build @@ -11,8 +11,8 @@ executables += [ 'sources' : files( 'imdsd.c', 'imds-util.c' - ) + import_curl_util_c, - 'dependencies' : [ libcurl ], + ) + curl_util_c, + 'dependencies' : [libcurl], }, libexec_template + { 'name' : 'systemd-imds', diff --git a/src/import/meson.build b/src/import/meson.build index a98604d491..1fab9afd0b 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -1,7 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -import_curl_util_c = files('curl-util.c') - if conf.get('ENABLE_IMPORTD') != 1 subdir_done() endif @@ -35,7 +33,7 @@ executables += [ 'pull-oci.c', 'pull-raw.c', 'pull-tar.c', - ) + import_curl_util_c, + ) + curl_util_c, 'objects' : ['systemd-importd'], 'dependencies' : common_deps + [ libopenssl, diff --git a/src/journal-remote/journal-upload.c b/src/journal-remote/journal-upload.c index e2038dd4b6..88f5cf7139 100644 --- a/src/journal-remote/journal-upload.c +++ b/src/journal-remote/journal-upload.c @@ -11,6 +11,7 @@ #include "alloc-util.h" #include "build.h" #include "conf-parser.h" +#include "curl-util.h" #include "daemon-util.h" #include "env-file.h" #include "extract-word.h" @@ -81,18 +82,6 @@ static void close_fd_input(Uploader *u); #define STATE_FILE "/var/lib/systemd/journal-upload/state" -#define easy_setopt(curl, log_level, opt, value) ({ \ - CURLcode code = curl_easy_setopt(ASSERT_PTR(curl), opt, value); \ - if (code) \ - log_full(log_level, \ - "curl_easy_setopt %s failed: %s", \ - #opt, curl_easy_strerror(code)); \ - !code; \ -}) - -DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURL*, curl_easy_cleanup, NULL); -DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct curl_slist*, curl_slist_free_all, NULL); - static size_t output_callback(char *buf, size_t size, size_t nmemb, diff --git a/src/import/curl-util.c b/src/shared/curl-util.c similarity index 100% rename from src/import/curl-util.c rename to src/shared/curl-util.c diff --git a/src/import/curl-util.h b/src/shared/curl-util.h similarity index 69% rename from src/import/curl-util.h rename to src/shared/curl-util.h index b48eeb9c43..6b922d5003 100644 --- a/src/import/curl-util.h +++ b/src/shared/curl-util.h @@ -5,6 +5,19 @@ #include "shared-forward.h" +#define easy_setopt(curl, log_level, opt, value) ({ \ + CURLcode code = curl_easy_setopt(ASSERT_PTR(curl), opt, value); \ + if (code) \ + log_full(log_level, \ + "curl_easy_setopt %s failed: %s", \ + #opt, curl_easy_strerror(code)); \ + code == CURLE_OK; \ +}) + +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURL*, curl_easy_cleanup, NULL); +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURLM*, curl_multi_cleanup, NULL); +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct curl_slist*, curl_slist_free_all, NULL); + typedef struct CurlGlue CurlGlue; typedef struct CurlGlue { @@ -30,7 +43,3 @@ void curl_glue_remove_and_free(CurlGlue *g, CURL *c); struct curl_slist *curl_slist_new(const char *first, ...) _sentinel_; int curl_header_strdup(const void *contents, size_t sz, const char *field, char **value); int curl_parse_http_time(const char *t, usec_t *ret); - -DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURL*, curl_easy_cleanup, NULL); -DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURLM*, curl_multi_cleanup, NULL); -DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct curl_slist*, curl_slist_free_all, NULL); diff --git a/src/shared/meson.build b/src/shared/meson.build index 22dccf0e2a..efa0dc05ad 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -256,6 +256,9 @@ if get_option('tests') != 'false' shared_sources += files('tests.c') endif +# A small shared file that is is linked into a few places +curl_util_c = files('curl-util.c') + syscall_list_inc = custom_target( input : syscall_list_txt, output : 'syscall-list.inc', From b63373a65657ef610622590a00dd5a9452873e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 31 Mar 2026 13:08:03 +0200 Subject: [PATCH 04/17] meson: use a convenience lib for curl-util.c Previously we compiled curl-util.c at least two times, and then also shared it using the extract+object. Let's build a static "convenience lib" for it. (Using extract+object everywhere is not possible because the different places where it is used are conditionalized independently so we don't have a single "source" that is always available.) --- src/imds/meson.build | 3 ++- src/import/meson.build | 3 ++- src/shared/meson.build | 18 +++++++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/imds/meson.build b/src/imds/meson.build index cb919fec61..29fa878eab 100644 --- a/src/imds/meson.build +++ b/src/imds/meson.build @@ -11,7 +11,8 @@ executables += [ 'sources' : files( 'imdsd.c', 'imds-util.c' - ) + curl_util_c, + ), + 'link_with' : [libcurlutil_static, libshared], 'dependencies' : [libcurl], }, libexec_template + { diff --git a/src/import/meson.build b/src/import/meson.build index 1fab9afd0b..13c90d7937 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -33,8 +33,9 @@ executables += [ 'pull-oci.c', 'pull-raw.c', 'pull-tar.c', - ) + curl_util_c, + ), 'objects' : ['systemd-importd'], + 'link_with' : [libcurlutil_static, libshared], 'dependencies' : common_deps + [ libopenssl, ], diff --git a/src/shared/meson.build b/src/shared/meson.build index efa0dc05ad..8fb14f9fef 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -256,9 +256,6 @@ if get_option('tests') != 'false' shared_sources += files('tests.c') endif -# A small shared file that is is linked into a few places -curl_util_c = files('curl-util.c') - syscall_list_inc = custom_target( input : syscall_list_txt, output : 'syscall-list.inc', @@ -458,3 +455,18 @@ libshared_fdisk = static_library( userspace], c_args : ['-fvisibility=default'], build_by_default : false) + +# A small shared file that is linked into a few places. +# It is not part of libshared because this code needs libcurl and +# we don't want to link libshared to libcurl. +if conf.get('HAVE_LIBCURL') == 1 + libcurlutil_static = static_library( + 'curl-util', + 'curl-util.c', + implicit_include_directories : false, + dependencies : [userspace, libcurl], + include_directories : includes, + build_by_default : false) +else + libcurlutil_static = [] +endif From 93ee4308d99847f67f638f314a186b8f15bf08dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Apr 2026 13:25:08 +0200 Subject: [PATCH 05/17] journal-upload: require TLS 1.2 as the minimum version RFC 8996 says: > This document formally deprecates Transport Layer Security (TLS) > versions 1.0 (RFC 2246) and 1.1 (RFC 4346). Accordingly, those > documents have been moved to Historic status. These versions lack > support for current and recommended cryptographic algorithms and > mechanisms, and various government and industry profiles of > applications using TLS now mandate avoiding these old TLS versions. > TLS version 1.2 became the recommended version for IETF protocols in > 2008 (subsequently being obsoleted by TLS version 1.3 in 2018), > providing sufficient time to transition away from older versions. > Removing support for older versions from implementations reduces the > attack surface, reduces opportunity for misconfiguration, and > streamlines library and product maintenance. This code probably only talks to our own receiver which uses libmicrohttpd. That in turn delegates to GnuTLS, which supports 1.2, 1.3, 3.0, etc. --- src/journal-remote/journal-upload.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/journal-remote/journal-upload.c b/src/journal-remote/journal-upload.c index 88f5cf7139..99de0fc93f 100644 --- a/src/journal-remote/journal-upload.c +++ b/src/journal-remote/journal-upload.c @@ -302,8 +302,8 @@ int start_upload(Uploader *u, return -EXFULL; } - if (arg_key || arg_trust) - (void) easy_setopt(curl, LOG_WARNING, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1); + if (startswith(u->url, "https://")) + (void) easy_setopt(curl, LOG_WARNING, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); u->easy = TAKE_PTR(curl); } else { From 2d2b4cc009f65082dfd28ac5b2a8be1ec68a06aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Apr 2026 13:32:33 +0200 Subject: [PATCH 06/17] shared/facts: use SD_JSON_BUILD_PAIR_VARIANT in one more place Suggested in review by Claude. --- src/shared/facts.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/facts.c b/src/shared/facts.c index 7554126e2b..fa16c7b7cb 100644 --- a/src/shared/facts.c +++ b/src/shared/facts.c @@ -123,7 +123,7 @@ static int fact_build_send(FactFamilyContext *context, const char *object, sd_js return sd_varlink_replybo(context->link, SD_JSON_BUILD_PAIR_STRING("name", context->fact_family->name), JSON_BUILD_PAIR_STRING_NON_EMPTY("object", object), - SD_JSON_BUILD_PAIR("value", SD_JSON_BUILD_VARIANT(value))); + SD_JSON_BUILD_PAIR_VARIANT("value", value)); } int fact_build_send_string(FactFamilyContext *context, const char *object, const char *value) { From 267b16f33c5636617927f15d7ae6b945c862a587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 14 Apr 2026 08:28:28 +0200 Subject: [PATCH 07/17] basic/iovec-wrapper: drop unused code All non-test users iovec_wrapper define the struct as a field in a bigger structure, so we never free it individually. Let's simplify the code and assume it is never null. --- src/basic/iovec-wrapper.c | 23 +---------------------- src/basic/iovec-wrapper.h | 6 ------ src/test/test-log.c | 7 +++---- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/src/basic/iovec-wrapper.c b/src/basic/iovec-wrapper.c index b4519f808d..7f91f5e893 100644 --- a/src/basic/iovec-wrapper.c +++ b/src/basic/iovec-wrapper.c @@ -7,10 +7,6 @@ #include "iovec-wrapper.h" #include "string-util.h" -struct iovec_wrapper *iovw_new(void) { - return new0(struct iovec_wrapper, 1); -} - void iovw_done(struct iovec_wrapper *iovw) { assert(iovw); @@ -27,22 +23,6 @@ void iovw_done_free(struct iovec_wrapper *iovw) { iovw_done(iovw); } -struct iovec_wrapper *iovw_free_free(struct iovec_wrapper *iovw) { - if (!iovw) - return NULL; - - iovw_done_free(iovw); - return mfree(iovw); -} - -struct iovec_wrapper *iovw_free(struct iovec_wrapper *iovw) { - if (!iovw) - return NULL; - - iovw_done(iovw); - return mfree(iovw); -} - int iovw_put(struct iovec_wrapper *iovw, void *data, size_t len) { assert(iovw); @@ -118,8 +98,7 @@ void iovw_rebase(struct iovec_wrapper *iovw, void *old, void *new) { } size_t iovw_size(const struct iovec_wrapper *iovw) { - if (!iovw) - return 0; + assert(iovw); return iovec_total_size(iovw->iovec, iovw->count); } diff --git a/src/basic/iovec-wrapper.h b/src/basic/iovec-wrapper.h index 94b39feb15..bfe739ecf1 100644 --- a/src/basic/iovec-wrapper.h +++ b/src/basic/iovec-wrapper.h @@ -8,12 +8,6 @@ struct iovec_wrapper { size_t count; }; -struct iovec_wrapper *iovw_new(void); -struct iovec_wrapper *iovw_free(struct iovec_wrapper *iovw); -struct iovec_wrapper *iovw_free_free(struct iovec_wrapper *iovw); - -DEFINE_TRIVIAL_CLEANUP_FUNC(struct iovec_wrapper*, iovw_free_free); - void iovw_done_free(struct iovec_wrapper *iovw); void iovw_done(struct iovec_wrapper *iovw); diff --git a/src/test/test-log.c b/src/test/test-log.c index f77ed205a7..a62f54006e 100644 --- a/src/test/test-log.c +++ b/src/test/test-log.c @@ -261,13 +261,12 @@ static void test_log_context(void) { IOVEC_MAKE_STRING("ABC=def"), IOVEC_MAKE_STRING("GHI=jkl"), }; - _cleanup_free_ struct iovec_wrapper *iovw = NULL; - ASSERT_NOT_NULL(iovw = iovw_new()); - ASSERT_OK(iovw_consume(iovw, strdup("MNO=pqr"), STRLEN("MNO=pqr") + 1)); + struct iovec_wrapper iovw = {}; + ASSERT_OK(iovw_consume(&iovw, strdup("MNO=pqr"), STRLEN("MNO=pqr") + 1)); LOG_CONTEXT_PUSH_IOV(iov, ELEMENTSOF(iov)); LOG_CONTEXT_PUSH_IOV(iov, ELEMENTSOF(iov)); - LOG_CONTEXT_CONSUME_IOV(iovw->iovec, iovw->count); + LOG_CONTEXT_CONSUME_IOV(iovw.iovec, iovw.count); LOG_CONTEXT_PUSH("STU=vwx"); ASSERT_EQ(log_context_num_contexts(), 3U); From f32af5c69ab71176a70c187b380e9689de834fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 13 Apr 2026 21:41:32 +0200 Subject: [PATCH 08/17] basic/iovec-wrapper: add iovw_append and iovw_to_cstring Existing iovw_append is renamed to iovw_append_iovw. iovw_consume is made noninline. --- src/basic/iovec-wrapper.c | 53 ++++++++++++++++++++++++++++++- src/basic/iovec-wrapper.h | 15 +++------ src/coredump/coredump-backtrace.c | 2 +- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/basic/iovec-wrapper.c b/src/basic/iovec-wrapper.c index 7f91f5e893..408985763e 100644 --- a/src/basic/iovec-wrapper.c +++ b/src/basic/iovec-wrapper.c @@ -41,6 +41,28 @@ int iovw_put(struct iovec_wrapper *iovw, void *data, size_t len) { return 0; } +int iovw_append(struct iovec_wrapper *iovw, const void *data, size_t len) { + if (len == 0) + return 0; + + void *c = memdup(data, len); + if (!c) + return -ENOMEM; + + return iovw_put(iovw, c, len); +} + +int iovw_consume(struct iovec_wrapper *iovw, void *data, size_t len) { + /* Move data into iovw or free on error */ + int r; + + r = iovw_put(iovw, data, len); + if (r < 0) + free(data); + + return r; +} + int iovw_put_string_field_full(struct iovec_wrapper *iovw, bool replace, const char *field, const char *value) { _cleanup_free_ char *x = NULL; int r; @@ -103,7 +125,7 @@ size_t iovw_size(const struct iovec_wrapper *iovw) { return iovec_total_size(iovw->iovec, iovw->count); } -int iovw_append(struct iovec_wrapper *target, const struct iovec_wrapper *source) { +int iovw_append_iovw(struct iovec_wrapper *target, const struct iovec_wrapper *source) { size_t original_count; int r; @@ -139,3 +161,32 @@ rollback: target->count = original_count; return r; } + +char* iovw_to_cstring(const struct iovec_wrapper *iovw) { + size_t size; + char *p, *ans; + + assert(iovw); + + /* Squish a series of iovecs into a C string. Embedded NULs are not allowed. + * The caller is expected to filter them out when populating the data. */ + + size = iovw_size(iovw); + if (size == SIZE_MAX) + return NULL; /* Prevent theoretical overflow */ + size ++; + + p = ans = new(char, size); + if (!ans) + return NULL; + + FOREACH_ARRAY(iovec, iovw->iovec, iovw->count) { + assert(!memchr(iovec->iov_base, 0, iovec->iov_len)); + + p = mempcpy(p, iovec->iov_base, iovec->iov_len); + } + + *p = '\0'; + + return ans; +} diff --git a/src/basic/iovec-wrapper.h b/src/basic/iovec-wrapper.h index bfe739ecf1..6f0c9af43c 100644 --- a/src/basic/iovec-wrapper.h +++ b/src/basic/iovec-wrapper.h @@ -12,16 +12,8 @@ void iovw_done_free(struct iovec_wrapper *iovw); void iovw_done(struct iovec_wrapper *iovw); int iovw_put(struct iovec_wrapper *iovw, void *data, size_t len); -static inline int iovw_consume(struct iovec_wrapper *iovw, void *data, size_t len) { - /* Move data into iovw or free on error */ - int r; - - r = iovw_put(iovw, data, len); - if (r < 0) - free(data); - - return r; -} +int iovw_append(struct iovec_wrapper *iovw, const void *data, size_t len); +int iovw_consume(struct iovec_wrapper *iovw, void *data, size_t len); static inline bool iovw_isempty(const struct iovec_wrapper *iovw) { return !iovw || iovw->count == 0; @@ -40,4 +32,5 @@ int iovw_put_string_fieldf_full(struct iovec_wrapper *iovw, bool replace, const int iovw_put_string_field_free(struct iovec_wrapper *iovw, const char *field, char *value); void iovw_rebase(struct iovec_wrapper *iovw, void *old, void *new); size_t iovw_size(const struct iovec_wrapper *iovw); -int iovw_append(struct iovec_wrapper *target, const struct iovec_wrapper *source); +int iovw_append_iovw(struct iovec_wrapper *target, const struct iovec_wrapper *source); +char* iovw_to_cstring(const struct iovec_wrapper *iovw); diff --git a/src/coredump/coredump-backtrace.c b/src/coredump/coredump-backtrace.c index 0a2dec2313..b8aa880f8e 100644 --- a/src/coredump/coredump-backtrace.c +++ b/src/coredump/coredump-backtrace.c @@ -50,7 +50,7 @@ int coredump_backtrace(int argc, char *argv[]) { } else { /* The imported iovecs are not supposed to be freed by us so let's copy and merge them at the * end of the array. */ - r = iovw_append(&context.iovw, &importer.iovw); + r = iovw_append_iovw(&context.iovw, &importer.iovw); if (r < 0) return r; } From 5adccf5034a95caaa259908ad4759d52751967ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 15 Apr 2026 00:48:31 +0200 Subject: [PATCH 09/17] test: add test-iovec-wrapper Tests the old code in iovec-wrapper and the two new functions. Co-developed-by: Claude Opus 4.6 --- src/test/meson.build | 1 + src/test/test-iovec-wrapper.c | 245 ++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 src/test/test-iovec-wrapper.c diff --git a/src/test/meson.build b/src/test/meson.build index fb0aac27f8..0963924492 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -130,6 +130,7 @@ simple_tests += files( 'test-install-root.c', 'test-io-util.c', 'test-iovec-util.c', + 'test-iovec-wrapper.c', 'test-journal-importer.c', 'test-kbd-util.c', 'test-label.c', diff --git a/src/test/test-iovec-wrapper.c b/src/test/test-iovec-wrapper.c new file mode 100644 index 0000000000..35b9396572 --- /dev/null +++ b/src/test/test-iovec-wrapper.c @@ -0,0 +1,245 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "alloc-util.h" +#include "iovec-wrapper.h" +#include "tests.h" + +TEST(iovw_put) { + _cleanup_(iovw_done) struct iovec_wrapper iovw = {}; + + /* Zero-length insertions are no-ops and do not touch the data pointer */ + ASSERT_OK_ZERO(iovw_put(&iovw, NULL, 0)); + ASSERT_OK_ZERO(iovw_put(&iovw, (char*) "foo", 0)); + ASSERT_EQ(iovw.count, 0U); + + ASSERT_OK(iovw_put(&iovw, (char*) "foo", 3)); + ASSERT_OK(iovw_put(&iovw, (char*) "barbar", 6)); + ASSERT_OK(iovw_put(&iovw, (char*) "q", 1)); + ASSERT_EQ(iovw.count, 3U); + + ASSERT_EQ(iovw.iovec[0].iov_len, 3U); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "foo", 3), 0); + ASSERT_EQ(iovw.iovec[1].iov_len, 6U); + ASSERT_EQ(memcmp(iovw.iovec[1].iov_base, "barbar", 6), 0); + ASSERT_EQ(iovw.iovec[2].iov_len, 1U); + ASSERT_EQ(memcmp(iovw.iovec[2].iov_base, "q", 1), 0); +} + +TEST(iovw_append) { + _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {}; + + /* iovw_append copies the data; the wrapper owns the copies. */ + char buf[4] = { 'o', 'n', 'e', '\0' }; + ASSERT_OK(iovw_append(&iovw, buf, 3)); + ASSERT_EQ(iovw.count, 1U); + ASSERT_EQ(iovw.iovec[0].iov_len, 3U); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "one", 3), 0); + + /* Insert with a NUL */ + ASSERT_OK_ZERO(iovw_append(&iovw, buf, 4)); + ASSERT_EQ(iovw.count, 2U); + ASSERT_EQ(iovw.iovec[1].iov_len, 4U); + ASSERT_EQ(memcmp(iovw.iovec[1].iov_base, "one\0", 4), 0); + + /* Mutating the caller's buffer does not affect what's stored */ + memset(buf, 'X', sizeof buf); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "one", 3), 0); +} + +TEST(iovw_consume) { + _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {}; + + char *p = strdup("consumed"); + ASSERT_NOT_NULL(p); + ASSERT_OK(iovw_consume(&iovw, p, strlen(p))); + ASSERT_EQ(iovw.count, 1U); + /* iovw_consume moves ownership in place, no copy */ + ASSERT_PTR_EQ(iovw.iovec[0].iov_base, p); + + /* Zero-length: iovw_put returns 0 without adding anything, and does not free the payload. + * Confirm by strdup'ing something and explicitly freeing it afterwards. */ + _cleanup_free_ char *q = strdup(""); + ASSERT_NOT_NULL(q); + ASSERT_OK_ZERO(iovw_consume(&iovw, q, 0)); + ASSERT_EQ(iovw.count, 1U); +} + +TEST(iovw_isempty) { + ASSERT_TRUE(iovw_isempty(NULL)); + + _cleanup_(iovw_done) struct iovec_wrapper iovw = {}; + ASSERT_TRUE(iovw_isempty(&iovw)); + + ASSERT_OK(iovw_put(&iovw, (char*) "x", 1)); + ASSERT_FALSE(iovw_isempty(&iovw)); +} + +TEST(iovw_put_string_field) { + _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {}; + + ASSERT_OK(iovw_put_string_field(&iovw, "FOO=", "bar")); + ASSERT_OK(iovw_put_string_field(&iovw, "BAZ=", "quux")); + ASSERT_EQ(iovw.count, 2U); + + ASSERT_EQ(iovw.iovec[0].iov_len, strlen("FOO=bar")); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "FOO=bar", strlen("FOO=bar")), 0); + ASSERT_EQ(iovw.iovec[1].iov_len, strlen("BAZ=quux")); + ASSERT_EQ(memcmp(iovw.iovec[1].iov_base, "BAZ=quux", strlen("BAZ=quux")), 0); + + /* Non-replacing put: a second FOO= just appends rather than replacing */ + ASSERT_OK(iovw_put_string_field(&iovw, "FOO=", "second")); + ASSERT_EQ(iovw.count, 3U); + ASSERT_EQ(iovw.iovec[0].iov_len, strlen("FOO=bar")); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "FOO=bar", strlen("FOO=bar")), 0); + ASSERT_EQ(iovw.iovec[2].iov_len, strlen("FOO=second")); + ASSERT_EQ(memcmp(iovw.iovec[2].iov_base, "FOO=second", strlen("FOO=second")), 0); +} + +TEST(iovw_replace_string_field) { + _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {}; + + /* If the field does not exist yet, replace acts like put */ + ASSERT_OK(iovw_replace_string_field(&iovw, "A=", "1")); + ASSERT_EQ(iovw.count, 1U); + ASSERT_EQ(iovw.iovec[0].iov_len, strlen("A=1")); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "A=1", strlen("A=1")), 0); + + /* Replacing an existing field updates it in place */ + ASSERT_OK(iovw_replace_string_field(&iovw, "A=", "twentytwo")); + ASSERT_EQ(iovw.count, 1U); + ASSERT_EQ(iovw.iovec[0].iov_len, strlen("A=twentytwo")); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "A=twentytwo", strlen("A=twentytwo")), 0); + + /* Distinct field still appends */ + ASSERT_OK(iovw_replace_string_field(&iovw, "B=", "x")); + ASSERT_EQ(iovw.count, 2U); +} + +TEST(iovw_put_string_fieldf) { + _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {}; + + ASSERT_OK(iovw_put_string_fieldf(&iovw, "N=", "%d-%s", 42, "answer")); + ASSERT_EQ(iovw.count, 1U); + ASSERT_EQ(iovw.iovec[0].iov_len, strlen("N=42-answer")); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "N=42-answer", strlen("N=42-answer")), 0); + + /* Replacing variant */ + ASSERT_OK(iovw_replace_string_fieldf(&iovw, "N=", "%d", 7)); + ASSERT_EQ(iovw.count, 1U); + ASSERT_EQ(iovw.iovec[0].iov_len, strlen("N=7")); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "N=7", strlen("N=7")), 0); +} + +TEST(iovw_put_string_field_free) { + _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {}; + + /* iovw_put_string_field_free takes ownership of the value string (frees it on return). */ + char *v = strdup("hello"); + ASSERT_NOT_NULL(v); + ASSERT_OK(iovw_put_string_field_free(&iovw, "K=", v)); + ASSERT_EQ(iovw.count, 1U); + ASSERT_EQ(iovw.iovec[0].iov_len, strlen("K=hello")); + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "K=hello", strlen("K=hello")), 0); +} + +TEST(iovw_rebase) { + /* iovw_rebase shifts all iov_base pointers from an old base to a new base. Fabricate a + * stand-in "old base" and "new base" and a wrapper with offsets pointing into the old + * base, then verify they get rewritten to point into the new base. */ + + uint8_t old_base[64] = {}, new_base[64] = {}; + for (size_t i = 0; i < sizeof old_base; i++) { + old_base[i] = (uint8_t) i; + new_base[i] = (uint8_t) (100 + i); + } + + _cleanup_(iovw_done) struct iovec_wrapper iovw = {}; + + ASSERT_OK(iovw_put(&iovw, old_base + 0, 4)); + ASSERT_OK(iovw_put(&iovw, old_base + 10, 2)); + ASSERT_OK(iovw_put(&iovw, old_base + 30, 8)); + ASSERT_EQ(iovw.count, 3U); + + iovw_rebase(&iovw, old_base, new_base); + + ASSERT_PTR_EQ(iovw.iovec[0].iov_base, new_base + 0); + ASSERT_PTR_EQ(iovw.iovec[1].iov_base, new_base + 10); + ASSERT_PTR_EQ(iovw.iovec[2].iov_base, new_base + 30); + + /* Lengths are preserved */ + ASSERT_EQ(iovw.iovec[0].iov_len, 4U); + ASSERT_EQ(iovw.iovec[1].iov_len, 2U); + ASSERT_EQ(iovw.iovec[2].iov_len, 8U); + + /* And the contents through the new base match what we staged there */ + ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, new_base + 0, 4), 0); + ASSERT_EQ(memcmp(iovw.iovec[1].iov_base, new_base + 10, 2), 0); + ASSERT_EQ(memcmp(iovw.iovec[2].iov_base, new_base + 30, 8), 0); +} + +TEST(iovw_size) { + _cleanup_(iovw_done) struct iovec_wrapper iovw = {}; + ASSERT_EQ(iovw_size(&iovw), 0U); + + ASSERT_OK(iovw_put(&iovw, (char*) "abcd", 4)); + ASSERT_OK(iovw_put(&iovw, (char*) "efghij", 6)); + ASSERT_OK(iovw_put(&iovw, (char*) "kl", 2)); + ASSERT_EQ(iovw_size(&iovw), 12U); +} + +TEST(iovw_append_iovw) { + _cleanup_(iovw_done_free) struct iovec_wrapper target = {}; + _cleanup_(iovw_done) struct iovec_wrapper source = {}; + + /* Appending an empty/NULL source is a no-op */ + ASSERT_OK_ZERO(iovw_append_iovw(&target, NULL)); + ASSERT_OK_ZERO(iovw_append_iovw(&target, &source)); + ASSERT_EQ(target.count, 0U); + + ASSERT_OK(iovw_put(&source, (char*) "one", 3)); + ASSERT_OK(iovw_put(&source, (char*) "twotwo", 6)); + ASSERT_EQ(source.count, 2U); + + /* Pre-seed target with one entry to check that append adds on top rather than replacing */ + char *seed = strdup("zero"); + ASSERT_NOT_NULL(seed); + ASSERT_OK(iovw_put(&target, seed, strlen(seed))); + + ASSERT_OK(iovw_append_iovw(&target, &source)); + ASSERT_EQ(target.count, 3U); + + /* Appended entries must be fresh copies, not aliases of the source entries */ + ASSERT_TRUE(target.iovec[1].iov_base != source.iovec[0].iov_base); + ASSERT_TRUE(target.iovec[2].iov_base != source.iovec[1].iov_base); + + ASSERT_EQ(target.iovec[1].iov_len, 3U); + ASSERT_EQ(memcmp(target.iovec[1].iov_base, "one", 3), 0); + ASSERT_EQ(target.iovec[2].iov_len, 6U); + ASSERT_EQ(memcmp(target.iovec[2].iov_base, "twotwo", 6), 0); + + /* Source is unchanged */ + ASSERT_EQ(source.count, 2U); +} + +TEST(iovw_to_cstring) { + _cleanup_(iovw_done) struct iovec_wrapper iovw = {}; + _cleanup_free_ char *s; + + /* Empty wrapper → empty string */ + s = iovw_to_cstring(&iovw); + ASSERT_NOT_NULL(s); + ASSERT_STREQ(s, ""); + s = mfree(s); + + ASSERT_OK(iovw_put(&iovw, (char*) "foo", 3)); + ASSERT_OK(iovw_put(&iovw, (char*) "/", 1)); + ASSERT_OK(iovw_put(&iovw, (char*) "bar", 3)); + + s = iovw_to_cstring(&iovw); + ASSERT_NOT_NULL(s); + ASSERT_STREQ(s, "foo/bar"); +} + +DEFINE_TEST_MAIN(LOG_INFO); From 067aa9b767954d134b6f69a5b97ebbd19bbb9697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 13 Apr 2026 21:46:12 +0200 Subject: [PATCH 10/17] test-string-util: test empty_to_null on a char array Unfortunately empty_to_null(t) where t is char[] fails. But it works with &t[0]. --- src/test/test-string-util.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/test/test-string-util.c b/src/test/test-string-util.c index 4f12ec710c..31b3d862f4 100644 --- a/src/test/test-string-util.c +++ b/src/test/test-string-util.c @@ -617,6 +617,28 @@ TEST(split_pair) { ASSERT_STREQ(b, "="); } +TEST(empty_to_null) { + const char *s = "asdf", *n = NULL, *e = ""; + char *t = (char*) "asdf"; + const char p[] = "asdf"; + char q[] = "asdf"; + + /* empty_to_null cannot be used with constant strings, e.g. + * empty_to_null("") fails with 'error: cast specifies array type'. */ + + ASSERT_NULL(empty_to_null(NULL)); + ASSERT_NULL(empty_to_null(n)); + ASSERT_NULL(empty_to_null(e)); + ASSERT_STREQ(empty_to_null(s), "asdf"); + ASSERT_NULL(empty_to_null(s + 4)); + ASSERT_STREQ(empty_to_null(t), "asdf"); + ASSERT_NULL(empty_to_null(t + 4)); + ASSERT_STREQ(empty_to_null(&p[0]), "asdf"); + ASSERT_NULL(empty_to_null(&p[0] + 4)); + ASSERT_STREQ(empty_to_null(&q[0]), "asdf"); + ASSERT_NULL(empty_to_null(&q[0] + 4)); +} + TEST(first_word) { assert_se(first_word("Hello", "")); assert_se(first_word("Hello", "Hello")); From c861496cd3c90b93673e6f5431496325fef8a572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 13 Apr 2026 21:46:44 +0200 Subject: [PATCH 11/17] shared/curl-util: add curl_append_to_header --- src/imds/imdsd.c | 18 ++++++------------ src/shared/curl-util.c | 17 +++++++++++++++++ src/shared/curl-util.h | 1 + 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/imds/imdsd.c b/src/imds/imdsd.c index 7831d1c68c..2b7bf11335 100644 --- a/src/imds/imdsd.c +++ b/src/imds/imdsd.c @@ -1112,20 +1112,14 @@ static int context_acquire_data(Context *c) { if (!token_header) return context_log_oom(c); - struct curl_slist *n = curl_slist_append(c->request_header_data, token_header); - if (!n) - return context_log_oom(c); - - c->request_header_data = n; + r = curl_append_to_header(&c->request_header_data, STRV_MAKE(token_header)); + if (r < 0) + return context_log_errno(c, LOG_ERR, r, "Failed to create curl header: %m"); } - STRV_FOREACH(i, arg_extra_header) { - struct curl_slist *n = curl_slist_append(c->request_header_data, *i); - if (!n) - return context_log_oom(c); - - c->request_header_data = n; - } + r = curl_append_to_header(&c->request_header_data, arg_extra_header); + if (r < 0) + return context_log_errno(c, LOG_ERR, r, "Failed to create curl header: %m"); if (c->request_header_data) if (curl_easy_setopt(c->curl_data, CURLOPT_HTTPHEADER, c->request_header_data) != CURLE_OK) diff --git a/src/shared/curl-util.c b/src/shared/curl-util.c index 52321d08e1..405c083afc 100644 --- a/src/shared/curl-util.c +++ b/src/shared/curl-util.c @@ -8,6 +8,7 @@ #include "hashmap.h" #include "log.h" #include "string-util.h" +#include "strv.h" #include "time-util.h" #include "version.h" @@ -407,3 +408,19 @@ int curl_parse_http_time(const char *t, usec_t *ret) { return 0; } + +int curl_append_to_header(struct curl_slist **list, char **headers) { + /* This function leaves 'list' modified on partial failure. + * Input/output param list may point to NULL. */ + + assert(list); + + STRV_FOREACH(h, headers) { + struct curl_slist *l = curl_slist_append(*list, *h); + if (!l) + return -ENOMEM; + *list = l; + } + + return 0; +} diff --git a/src/shared/curl-util.h b/src/shared/curl-util.h index 6b922d5003..03b6ba7d21 100644 --- a/src/shared/curl-util.h +++ b/src/shared/curl-util.h @@ -43,3 +43,4 @@ void curl_glue_remove_and_free(CurlGlue *g, CURL *c); struct curl_slist *curl_slist_new(const char *first, ...) _sentinel_; int curl_header_strdup(const void *contents, size_t sz, const char *field, char **value); int curl_parse_http_time(const char *t, usec_t *ret); +int curl_append_to_header(struct curl_slist **list, char **headers); From 5bbbe210a4e3856385d95e16074d8aa98cff909b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 6 Mar 2026 11:36:13 +0100 Subject: [PATCH 12/17] report: add basic upload functionality --- src/report/meson.build | 7 +- src/report/report-upload.c | 200 +++++++++++++++++++++++++++++++++++++ src/report/report.c | 84 +++++++++++----- src/report/report.h | 35 +++++++ 4 files changed, 302 insertions(+), 24 deletions(-) create mode 100644 src/report/report-upload.c create mode 100644 src/report/report.h diff --git a/src/report/meson.build b/src/report/meson.build index 36227bed9f..6e0c231be3 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -4,7 +4,12 @@ executables += [ libexec_template + { 'name' : 'systemd-report', 'public' : true, - 'sources' : files('report.c'), + 'sources' : files( + 'report.c', + 'report-upload.c', + ), + 'link_with' : [libcurlutil_static, libshared], + 'dependencies' : [libcurl], }, libexec_template + { diff --git a/src/report/report-upload.c b/src/report/report-upload.c new file mode 100644 index 0000000000..897d7d1159 --- /dev/null +++ b/src/report/report-upload.c @@ -0,0 +1,200 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-json.h" + +#include "log.h" +#include "report.h" +#include "string-util.h" +#include "strv.h" +#include "time-util.h" +#include "utf8.h" +#include "version.h" + +#if HAVE_LIBCURL +#include "curl-util.h" +#include /* Sadly this fails if ordered first. */ + +static size_t output_callback(char *buf, + size_t size, + size_t nmemb, + void *userp) { + + Context *context = ASSERT_PTR(userp); + int r; + + assert(size == 1); /* The docs say that this is always true. */ + + log_debug("Got an answer from the server (%zu bytes)", nmemb); + + if (nmemb != 0) { + if (memchr(buf, 0, nmemb)) { + log_warning("Server answer contains an embedded NUL, refusing."); + return 0; + } + + r = iovw_append(&context->upload_answer, buf, nmemb); + if (r < 0) { + log_warning("Failed to store server answer (%zu bytes): out of memory", nmemb); + return 0; /* Returning < nmemb signals failure */ + } + } + + return nmemb; +} + +static int build_json_report(Context *context, sd_json_variant **ret) { + /* Convert the variant array to a JSON report. */ + + assert(context); + assert(ret); + + usec_t ts = now(CLOCK_REALTIME); + int r; + + const char *ident; + if (IN_SET(context->action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS)) + ident = "metrics"; + else if (IN_SET(context->action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS)) + ident = "facts"; + else + assert_not_reached(); + + r = sd_json_buildo(ret, + SD_JSON_BUILD_PAIR("timestamp", + SD_JSON_BUILD_STRING(FORMAT_TIMESTAMP_STYLE(ts, TIMESTAMP_UTC))), + SD_JSON_BUILD_PAIR(ident, + SD_JSON_BUILD_VARIANT_ARRAY(context->metrics, context->n_metrics))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON data: %m"); + return 0; +} +#endif + +int upload_collected(Context *context) { +#if HAVE_LIBCURL + _cleanup_(curl_slist_free_allp) struct curl_slist *header = NULL; + char error[CURL_ERROR_SIZE] = {}; + _cleanup_free_ char *json = NULL; + int r; + + { + /* Convert our variant array to a JSON report. + * We won't need the JSON structure again, so free it quickly. */ + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *vl = NULL; + r = build_json_report(context, &vl); + if (r < 0) + return r; + + r = sd_json_variant_format(vl, /* flags= */ 0, &json); + if (r < 0) + return log_error_errno(r, "Failed to format JSON data: %m"); + } + + r = curl_append_to_header(&header, + STRV_MAKE("Content-Type: application/json", + "Accept: application/json")); + if (r < 0) + return log_error_errno(r, "Failed to create curl header: %m"); + + _cleanup_(curl_easy_cleanupp) CURL *curl = curl_easy_init(); + if (!curl) + return log_error_errno(SYNTHETIC_ERRNO(ENOSR), + "Call to curl_easy_init failed."); + + /* If configured, set a timeout for the curl operation. */ + if (arg_network_timeout_usec != USEC_INFINITY && + !easy_setopt(curl, LOG_ERR, CURLOPT_TIMEOUT, + (long) DIV_ROUND_UP(arg_network_timeout_usec, USEC_PER_SEC))) + return -EXFULL; + + /* Tell it to POST to the URL */ + if (!easy_setopt(curl, LOG_ERR, CURLOPT_POST, 1L)) + return -EXFULL; + + if (!easy_setopt(curl, LOG_ERR, CURLOPT_ERRORBUFFER, error)) + return -EXFULL; + + /* Where to write to */ + if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEFUNCTION, output_callback)) + return -EXFULL; + + if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEDATA, context)) + return -EXFULL; + + if (!easy_setopt(curl, LOG_ERR, CURLOPT_HTTPHEADER, header)) + return -EXFULL; + + if (DEBUG_LOGGING) + /* enable verbose for easier tracing */ + (void) easy_setopt(curl, LOG_WARNING, CURLOPT_VERBOSE, 1L); + + (void) easy_setopt(curl, LOG_WARNING, + CURLOPT_USERAGENT, "systemd-report " GIT_VERSION); + + if (!streq_ptr(arg_key, "-") && (arg_key || startswith(arg_url, "https://"))) { + if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLKEY, arg_key ?: REPORT_PRIV_KEY_FILE)) + return -EXFULL; + if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLCERT, arg_cert ?: REPORT_CERT_FILE)) + return -EXFULL; + } + + if (STRPTR_IN_SET(arg_trust, "-", "all")) { + log_info("Server certificate verification disabled."); + if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSL_VERIFYPEER, 0L)) + return -EUCLEAN; + if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSL_VERIFYHOST, 0L)) + return -EUCLEAN; + } else if (arg_trust || startswith(arg_url, "https://")) { + if (!easy_setopt(curl, LOG_ERR, CURLOPT_CAINFO, arg_trust ?: REPORT_TRUST_FILE)) + return -EXFULL; + } + + if (startswith(arg_url, "https://")) + (void) easy_setopt(curl, LOG_WARNING, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + + /* Upload to this place */ + if (!easy_setopt(curl, LOG_ERR, CURLOPT_URL, arg_url)) + return -EXFULL; + + if (!easy_setopt(curl, LOG_ERR, CURLOPT_POSTFIELDS, json)) + return -EXFULL; + + CURLcode code = curl_easy_perform(curl); + if (code != CURLE_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Upload to %s failed: %s", arg_url, + empty_to_null(&error[0]) ?: curl_easy_strerror(code)); + + long status; + code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); + if (code != CURLE_OK) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Failed to retrieve response code: %s", + curl_easy_strerror(code)); + + _cleanup_free_ char *ans = iovw_to_cstring(&context->upload_answer); + if (!ans) + return log_oom(); + + if (!utf8_is_valid(ans)) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Upload to %s failed with code %ld and an invalid UTF-8 answer.", + arg_url, status); + + if (status >= 300) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Upload to %s failed with code %ld: %s", + arg_url, status, strna(ans)); + if (status < 200) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Upload to %s finished with unexpected code %ld: %s", + arg_url, status, strna(ans)); + log_info("Upload to %s finished successfully with code %ld: %s", + arg_url, status, strna(ans)); + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Compiled without libcurl."); +#endif +} diff --git a/src/report/report.c b/src/report/report.c index 96ff28dccd..b02a48b28b 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -16,6 +16,7 @@ #include "path-lookup.h" #include "pretty-print.h" #include "recurse-dir.h" +#include "report.h" #include "runtime-scope.h" #include "set.h" #include "sort-util.h" @@ -35,27 +36,17 @@ static bool arg_legend = true; static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; static char **arg_matches = NULL; +char *arg_url = NULL; +char *arg_key = NULL; +char *arg_cert = NULL; +char *arg_trust = NULL; +usec_t arg_network_timeout_usec = TIMEOUT_USEC; STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep); - -typedef enum Action { - ACTION_LIST_METRICS, - ACTION_DESCRIBE_METRICS, - ACTION_LIST_FACTS, - ACTION_DESCRIBE_FACTS, - _ACTION_MAX, - _ACTION_INVALID = -EINVAL, -} Action; - -/* The structure for collected "metrics" or "facts". The fields - * are prefixed with just "metrics" for brevity. */ -typedef struct Context { - Action action; - sd_event *event; - Set *link_infos; - sd_json_variant **metrics; /* Collected metrics or facts for sorting */ - size_t n_metrics, n_skipped_metrics, n_invalid_metrics; -} Context; +STATIC_DESTRUCTOR_REGISTER(arg_url, freep); +STATIC_DESTRUCTOR_REGISTER(arg_key, freep); +STATIC_DESTRUCTOR_REGISTER(arg_cert, freep); +STATIC_DESTRUCTOR_REGISTER(arg_trust, freep); typedef struct LinkInfo { Context *context; @@ -76,9 +67,12 @@ static void context_done(Context *context) { if (!context) return; - set_free(context->link_infos); + context->event = sd_event_unref(context->event); + context->link_infos = set_free(context->link_infos); sd_json_variant_unref_many(context->metrics, context->n_metrics); - sd_event_unref(context->event); + context->metrics = NULL; + context->n_metrics = 0; + iovw_done_free(&context->upload_answer); } DEFINE_TRIVIAL_CLEANUP_FUNC(LinkInfo*, link_info_free); @@ -569,6 +563,7 @@ static int output_collected(Context *context) { } _cleanup_(table_unrefp) Table *table = NULL; + switch(context->action) { case ACTION_LIST_METRICS: @@ -771,7 +766,10 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) if (r < 0) return log_error_errno(r, "Failed to run event loop: %m"); - r = output_collected(&context); + if (arg_url) + r = upload_collected(&context); + else + r = output_collected(&context); if (r < 0) return r; } @@ -855,7 +853,10 @@ static int verb_facts(int argc, char *argv[], uintptr_t data, void *userdata) { if (r < 0) return log_error_errno(r, "Failed to run event loop: %m"); - r = output_collected(&context); + if (arg_url) + r = upload_collected(&context); + else + r = output_collected(&context); if (r < 0) return r; } @@ -1017,8 +1018,45 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION_COMMON_LOWERCASE_J: arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; break; + + OPTION_LONG("url", "URL", + "Upload to this address"): + r = free_and_strdup_warn(&arg_url, arg); + if (r < 0) + return r; + break; + + OPTION_LONG("key", "FILENAME", + "Specify key in PEM format (default: \"" REPORT_PRIV_KEY_FILE "\")"): + r = free_and_strdup_warn(&arg_key, arg); + if (r < 0) + return r; + break; + + OPTION_LONG("cert", "FILENAME", + "Specify certificate in PEM format (default: \"" REPORT_CERT_FILE "\")"): + r = free_and_strdup_warn(&arg_cert, arg); + if (r < 0) + return r; + break; + + OPTION_LONG("trust", "FILENAME|all", + "Specify CA certificate or disable checking (default: \"" REPORT_TRUST_FILE "\")"): + r = free_and_strdup_warn(&arg_trust, arg); + if (r < 0) + return r; + break; + + OPTION_LONG("network-timeout", "SEC", "Specify timeout for network upload operation"): + r = parse_sec(arg, &arg_network_timeout_usec); + if (r < 0) + return log_error_errno(r, "Failed to parse --network-timeout value: %s", arg); + break; } + if ((arg_url || arg_key || arg_cert || arg_trust) && !HAVE_LIBCURL) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Compiled without libcurl."); + *ret_args = option_parser_get_args(&state); return 1; } diff --git a/src/report/report.h b/src/report/report.h new file mode 100644 index 0000000000..69c9c5851b --- /dev/null +++ b/src/report/report.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +#include "iovec-wrapper.h" + +#define REPORT_PRIV_KEY_FILE CERTIFICATE_ROOT "/private/systemd-report.pem" +#define REPORT_CERT_FILE CERTIFICATE_ROOT "/certs/systemd-report.pem" +#define REPORT_TRUST_FILE CERTIFICATE_ROOT "/ca/trusted.pem" + +extern char *arg_url, *arg_key, *arg_cert, *arg_trust; +extern usec_t arg_network_timeout_usec; + +typedef enum Action { + ACTION_LIST_METRICS, + ACTION_DESCRIBE_METRICS, + ACTION_LIST_FACTS, + ACTION_DESCRIBE_FACTS, + _ACTION_MAX, + _ACTION_INVALID = -EINVAL, +} Action; + +/* The structure for collected "metrics" or "facts". The fields + * are prefixed with just "metrics" for brevity. */ +typedef struct Context { + Action action; + sd_event *event; + Set *link_infos; + sd_json_variant **metrics; /* Collected metrics or facts for sorting */ + size_t n_metrics, n_skipped_metrics, n_invalid_metrics; + struct iovec_wrapper upload_answer; +} Context; + +int upload_collected(Context *context); From fca491e03642ada516fe8386397aa166c92c284e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 16 Apr 2026 17:31:59 +0200 Subject: [PATCH 13/17] shared/webutil: reorder .c to match .h, mark one more function as _pure_ --- src/shared/web-util.c | 26 +++++++++++++------------- src/shared/web-util.h | 4 +--- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/shared/web-util.c b/src/shared/web-util.c index 628f0805bd..23f3004700 100644 --- a/src/shared/web-util.c +++ b/src/shared/web-util.c @@ -5,19 +5,6 @@ #include "utf8.h" #include "web-util.h" -bool http_etag_is_valid(const char *etag) { - if (isempty(etag)) - return false; - - if (!endswith(etag, "\"")) - return false; - - if (!STARTSWITH_SET(etag, "\"", "W/\"")) - return false; - - return true; -} - bool http_url_is_valid(const char *url) { const char *p; @@ -62,3 +49,16 @@ bool documentation_url_is_valid(const char *url) { return ascii_is_valid(p); } + +bool http_etag_is_valid(const char *etag) { + if (isempty(etag)) + return false; + + if (!endswith(etag, "\"")) + return false; + + if (!STARTSWITH_SET(etag, "\"", "W/\"")) + return false; + + return true; +} diff --git a/src/shared/web-util.h b/src/shared/web-util.h index 68f868ab0a..8a2f5c537b 100644 --- a/src/shared/web-util.h +++ b/src/shared/web-util.h @@ -5,7 +5,5 @@ bool http_url_is_valid(const char *url) _pure_; bool file_url_is_valid(const char *url) _pure_; - bool documentation_url_is_valid(const char *url) _pure_; - -bool http_etag_is_valid(const char *etag); +bool http_etag_is_valid(const char *etag) _pure_; From ce400ef2d956d268a72b1a7ebbc61d344697e4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 1 Apr 2026 07:32:46 +0200 Subject: [PATCH 14/17] report: add option to inject additional HTTP headers This is useful when debugging things. The option is named and implements the same logic as imdsd. --- src/imds/imdsd.c | 4 ---- src/report/report-upload.c | 4 ++++ src/report/report.c | 19 ++++++++++++++++++- src/report/report.h | 1 + src/shared/web-util.c | 7 +++++++ src/shared/web-util.h | 1 + 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/imds/imdsd.c b/src/imds/imdsd.c index 2b7bf11335..5ed7d1df33 100644 --- a/src/imds/imdsd.c +++ b/src/imds/imdsd.c @@ -2243,10 +2243,6 @@ static bool http_header_name_valid(const char *a) { return a && ascii_is_valid(a) && !string_has_cc(a, /* ok= */ NULL) && !strchr(a, ':'); } -static bool http_header_valid(const char *a) { - return a && ascii_is_valid(a) && !string_has_cc(a, /* ok= */ NULL) && strchr(a, ':'); -} - static int parse_argv(int argc, char *argv[]) { int r; diff --git a/src/report/report-upload.c b/src/report/report-upload.c index 897d7d1159..3022bd3049 100644 --- a/src/report/report-upload.c +++ b/src/report/report-upload.c @@ -97,6 +97,10 @@ int upload_collected(Context *context) { if (r < 0) return log_error_errno(r, "Failed to create curl header: %m"); + r = curl_append_to_header(&header, arg_extra_headers); + if (r < 0) + return log_error_errno(r, "Failed to create curl header: %m"); + _cleanup_(curl_easy_cleanupp) CURL *curl = curl_easy_init(); if (!curl) return log_error_errno(SYNTHETIC_ERRNO(ENOSR), diff --git a/src/report/report.c b/src/report/report.c index b02a48b28b..c45c4da7ea 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -26,6 +26,7 @@ #include "time-util.h" #include "varlink-idl-util.h" #include "verbs.h" +#include "web-util.h" #define METRICS_OR_FACTS_MAX 4096U #define METRICS_OR_FACTS_LINKS_MAX 128U @@ -40,6 +41,7 @@ char *arg_url = NULL; char *arg_key = NULL; char *arg_cert = NULL; char *arg_trust = NULL; +char **arg_extra_headers = NULL; usec_t arg_network_timeout_usec = TIMEOUT_USEC; STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep); @@ -47,6 +49,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_url, freep); STATIC_DESTRUCTOR_REGISTER(arg_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_cert, freep); STATIC_DESTRUCTOR_REGISTER(arg_trust, freep); +STATIC_DESTRUCTOR_REGISTER(arg_extra_headers, strv_freep); typedef struct LinkInfo { Context *context; @@ -1052,9 +1055,23 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { if (r < 0) return log_error_errno(r, "Failed to parse --network-timeout value: %s", arg); break; + + OPTION_LONG("extra-header", "NAME: VALUE", + "Inject additional header into the upload request"): + if (isempty(arg)) { + arg_extra_headers = strv_free(arg_extra_headers); + break; + } + + if (!http_header_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid HTTP header: %s", arg); + + if (strv_extend(&arg_extra_headers, arg) < 0) + return log_oom(); + break; } - if ((arg_url || arg_key || arg_cert || arg_trust) && !HAVE_LIBCURL) + if ((arg_url || arg_key || arg_cert || arg_trust || arg_extra_headers) && !HAVE_LIBCURL) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Compiled without libcurl."); *ret_args = option_parser_get_args(&state); diff --git a/src/report/report.h b/src/report/report.h index 69c9c5851b..4d7b5bdd3f 100644 --- a/src/report/report.h +++ b/src/report/report.h @@ -10,6 +10,7 @@ #define REPORT_TRUST_FILE CERTIFICATE_ROOT "/ca/trusted.pem" extern char *arg_url, *arg_key, *arg_cert, *arg_trust; +extern char **arg_extra_headers; extern usec_t arg_network_timeout_usec; typedef enum Action { diff --git a/src/shared/web-util.c b/src/shared/web-util.c index 23f3004700..bfcea982a9 100644 --- a/src/shared/web-util.c +++ b/src/shared/web-util.c @@ -50,6 +50,13 @@ bool documentation_url_is_valid(const char *url) { return ascii_is_valid(p); } +bool http_header_valid(const char *header) { + return header && + ascii_is_valid(header) && + !string_has_cc(header, /* ok= */ NULL) && + strchr(header, ':'); +} + bool http_etag_is_valid(const char *etag) { if (isempty(etag)) return false; diff --git a/src/shared/web-util.h b/src/shared/web-util.h index 8a2f5c537b..ec154c107a 100644 --- a/src/shared/web-util.h +++ b/src/shared/web-util.h @@ -6,4 +6,5 @@ bool http_url_is_valid(const char *url) _pure_; bool file_url_is_valid(const char *url) _pure_; bool documentation_url_is_valid(const char *url) _pure_; +bool http_header_valid(const char *header) _pure_; bool http_etag_is_valid(const char *etag) _pure_; From 0f3bd778e03965c11d6b67f33c8d030576cb2b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Apr 2026 14:09:25 +0200 Subject: [PATCH 15/17] test: add HTTP upload test for systemd-report Add a fake HTTP server (fake-report-server.py) that accepts JSON POST requests and validates the report structure, and test cases in TEST-74-AUX-UTILS.report.sh that exercise plain HTTP upload of both metrics and facts. Co-developed-by: Claude Opus 4.6 --- mkosi/mkosi.sanitizers/mkosi.postinst | 1 + .../fake-report-server.py | 56 +++++++++++++++++++ test/units/TEST-74-AUX-UTILS.report.sh | 14 +++++ 3 files changed, 71 insertions(+) create mode 100755 test/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py diff --git a/mkosi/mkosi.sanitizers/mkosi.postinst b/mkosi/mkosi.sanitizers/mkosi.postinst index 17c7d7bad9..1184331254 100755 --- a/mkosi/mkosi.sanitizers/mkosi.postinst +++ b/mkosi/mkosi.sanitizers/mkosi.postinst @@ -45,6 +45,7 @@ wrap=( /usr/lib/systemd/tests/testdata/TEST-74-AUX-UTILS.units/proxy-echo.py /usr/libexec/polkit-1/polkitd /usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-imds.py + /usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py agetty btrfs capsh diff --git a/test/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py b/test/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py new file mode 100755 index 0000000000..45cc34fa53 --- /dev/null +++ b/test/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py @@ -0,0 +1,56 @@ +#!/usr/bin/python3 +# SPDX-License-Identifier: LGPL-2.1-or-later + +import json, os, socket +from http.server import BaseHTTPRequestHandler, HTTPServer + +def sd_notify(state: str) -> bool: + notify_socket = os.environ.get("NOTIFY_SOCKET") + if not notify_socket: + return False + if notify_socket.startswith("@"): + notify_socket = "\0" + notify_socket[1:] + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock: + sock.sendto(state.encode(), notify_socket) + except OSError: + return False + return True + +class Handler(BaseHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + + # Validate JSON structure + try: + data = json.loads(body) + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + + print(f"JSON: {s if len(s := str(data)) < 80 else s[:40] + '…' + s[-40:]}") + + if "metrics" not in data and "facts" not in data: + self.send_error(400, "Missing 'metrics' or 'facts' field") + return + + response = json.dumps({"status": "ok"}).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", len(response)) + self.end_headers() + self.wfile.write(response) + + def log_message(self, fmt, *args): + print(f"{self.address_string()} - {fmt % args}") + +PORT = 8089 + +server = HTTPServer(("", PORT), Handler) +print(f"Serving on http://localhost:{PORT}/") +try: + sd_notify("READY=1") + server.serve_forever() +except KeyboardInterrupt: + print("\nStopped.") diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh index f92f1ed750..af134e980a 100755 --- a/test/units/TEST-74-AUX-UTILS.report.sh +++ b/test/units/TEST-74-AUX-UTILS.report.sh @@ -65,3 +65,17 @@ systemctl start systemd-report-basic.socket # Test facts via direct Varlink call on existing socket varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.List {} varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.Describe {} + +# Test HTTP upload (plain http) +at_exit() { + set +e + systemctl stop fake-report-server +} +trap at_exit EXIT + +systemd-run -p Type=notify --unit=fake-report-server \ + /usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py +systemctl status fake-report-server + +"$REPORT" metrics --url=http://localhost:8089/ +"$REPORT" facts --url=http://localhost:8089/ From b7ac32c2c38af30e27bb754cfce899a66209514d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Apr 2026 14:34:53 +0200 Subject: [PATCH 16/17] test: add HTTPS upload test for systemd-report Extend fake-report-server.py with optional --cert, --key, --port arguments for TLS support. Add a test case that generates a self-signed certificate and tests HTTPS upload of metrics and facts. Also exercise the --header param. Co-developed-by: Claude Opus 4.6 --- .../fake-report-server.py | 22 +++++++++++++++---- test/units/TEST-74-AUX-UTILS.report.sh | 21 +++++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/test/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py b/test/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py index 45cc34fa53..4875a00bad 100755 --- a/test/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py +++ b/test/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # SPDX-License-Identifier: LGPL-2.1-or-later -import json, os, socket +import argparse, json, os, socket, ssl from http.server import BaseHTTPRequestHandler, HTTPServer def sd_notify(state: str) -> bool: @@ -22,6 +22,10 @@ class Handler(BaseHTTPRequestHandler): length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) + # Check optional attribute + if auth := self.headers.get("Authorization"): + print(f"Authorization: {auth}") + # Validate JSON structure try: data = json.loads(body) @@ -45,10 +49,20 @@ class Handler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): print(f"{self.address_string()} - {fmt % args}") -PORT = 8089 +parser = argparse.ArgumentParser() +parser.add_argument("--port", type=int, default=8089) +parser.add_argument("--cert", help="TLS certificate file") +parser.add_argument("--key", help="TLS private key file") +args = parser.parse_args() -server = HTTPServer(("", PORT), Handler) -print(f"Serving on http://localhost:{PORT}/") +server = HTTPServer(("", args.port), Handler) +if args.cert and args.key: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.load_cert_chain(args.cert, args.key) + server.socket = ctx.wrap_socket(server.socket, server_side=True) + print(f"Serving on https://localhost:{args.port}/") +else: + print(f"Serving on http://localhost:{args.port}/") try: sd_notify("READY=1") server.serve_forever() diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh index af134e980a..53b83c4dd9 100755 --- a/test/units/TEST-74-AUX-UTILS.report.sh +++ b/test/units/TEST-74-AUX-UTILS.report.sh @@ -67,15 +67,30 @@ varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.Lis varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.Describe {} # Test HTTP upload (plain http) +FAKE_SERVER=/usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py +CERTDIR=$(mktemp -d) + at_exit() { set +e - systemctl stop fake-report-server + systemctl stop fake-report-server fake-report-server-tls + rm -rf "$CERTDIR" } trap at_exit EXIT -systemd-run -p Type=notify --unit=fake-report-server \ - /usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py +systemd-run -p Type=notify --unit=fake-report-server "$FAKE_SERVER" systemctl status fake-report-server "$REPORT" metrics --url=http://localhost:8089/ "$REPORT" facts --url=http://localhost:8089/ + +# Test HTTPS upload with generated TLS certificates +openssl req -x509 -newkey rsa:2048 -keyout "$CERTDIR/server.key" -out "$CERTDIR/server.crt" \ + -days 1 -nodes -subj "/CN=localhost" 2>/dev/null + +systemd-run -p Type=notify --unit=fake-report-server-tls \ + "$FAKE_SERVER" --cert="$CERTDIR/server.crt" --key="$CERTDIR/server.key" --port=8090 +systemctl status fake-report-server-tls + +"$REPORT" metrics --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" +"$REPORT" facts --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \ + --extra-header='Authorization: Bearer magic string' From baeb764635bda5d9bbec57b107f26efb6b115727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 16 Apr 2026 17:54:03 +0200 Subject: [PATCH 17/17] report: limit server answer to 1 MiB As suggested in review. --- src/report/report-upload.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/report/report-upload.c b/src/report/report-upload.c index 3022bd3049..218742f540 100644 --- a/src/report/report-upload.c +++ b/src/report/report-upload.c @@ -14,6 +14,8 @@ #include "curl-util.h" #include /* Sadly this fails if ordered first. */ +#define SERVER_ANSWER_MAX (1*1024*1024u) + static size_t output_callback(char *buf, size_t size, size_t nmemb, @@ -27,6 +29,13 @@ static size_t output_callback(char *buf, log_debug("Got an answer from the server (%zu bytes)", nmemb); if (nmemb != 0) { + size_t new_size = size_add(iovw_size(&context->upload_answer), nmemb); + + if (new_size > SERVER_ANSWER_MAX) { + log_warning("Server answer too long (%zu > %u), refusing.", new_size, SERVER_ANSWER_MAX); + return 0; + } + if (memchr(buf, 0, nmemb)) { log_warning("Server answer contains an embedded NUL, refusing."); return 0;