diff --git a/man/kernel-command-line.xml b/man/kernel-command-line.xml
index 03765010f0..8253f0fafd 100644
--- a/man/kernel-command-line.xml
+++ b/man/kernel-command-line.xml
@@ -736,6 +736,15 @@
is set in /etc/hostname. Note that this does not bar later runtime changes to
the hostname, it simply controls the initial hostname set during early boot.
+ The name may use the ? and $ wildcard patterns documented
+ in hostname5,
+ which are expanded deterministically from the machine ID each time the name is applied. The expanded
+ name is never persisted, hence it may change if the word lists change. Note that in the initrd the
+ word lists (and possibly the machine ID) are typically not available; the option is then ignored and
+ the hostname is instead determined from the other sources, i.e.
+ /etc/hostname or the system.hostname credential, falling back
+ to the default hostname if none of those apply.
+
diff --git a/src/shared/hostname-setup.c b/src/shared/hostname-setup.c
index 63464f26cf..aa04a1ceab 100644
--- a/src/shared/hostname-setup.c
+++ b/src/shared/hostname-setup.c
@@ -84,6 +84,30 @@ int shorten_overlong(const char *s, char **ret) {
return 1;
}
+static int validate_and_substitute_hostname_from_source(const char *raw, const char *source, char **ret) {
+ int r;
+
+ assert(raw);
+ assert(source);
+ assert(ret);
+
+ /* Validate a raw hostname value that may carry '?'/'$' wildcards, expand the wildcards, then validate the
+ * concrete result. Shared by the credential and kernel command line paths. */
+ if (!hostname_is_valid(raw, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN))
+ return log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Hostname specified %s is invalid, ignoring: %s", source, raw);
+
+ _cleanup_free_ char *substituted = NULL;
+ r = hostname_substitute_wildcards(raw, &substituted);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to substitute wildcards in hostname specified %s, ignoring: %m", source);
+
+ if (!hostname_is_valid(substituted, VALID_HOSTNAME_TRAILING_DOT)) /* check that the expanded hostname is valid */
+ return log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Hostname specified %s is invalid after expansion, ignoring: %s", source, substituted);
+
+ *ret = TAKE_PTR(substituted);
+ return 0;
+}
+
static int acquire_hostname_from_credential(char **ret) {
_cleanup_free_ char *cred = NULL;
int r;
@@ -96,22 +120,33 @@ static int acquire_hostname_from_credential(char **ret) {
if (r == 0) /* not found */
return -ENXIO;
- if (!hostname_is_valid(cred, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN))
- return log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Hostname specified in system.hostname credential is invalid, ignoring: %s", cred);
-
- _cleanup_free_ char *substituted = NULL;
- r = hostname_substitute_wildcards(cred, &substituted);
+ r = validate_and_substitute_hostname_from_source(cred, "in the system.hostname credential", ret);
if (r < 0)
- return log_warning_errno(r, "Failed to substitute wildcards in system.hostname credential, ignoring: %m");
-
- if (!hostname_is_valid(substituted, VALID_HOSTNAME_TRAILING_DOT)) /* check that the expanded hostname is valid */
- return log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Hostname from system.hostname credential is invalid after expansion, ignoring: %s", substituted);
+ return r;
log_info("Initializing hostname from credential.");
- *ret = TAKE_PTR(substituted);
return 0;
}
+static int acquire_hostname_from_cmdline(char **ret) {
+ _cleanup_free_ char *hn = NULL;
+ int r;
+
+ assert(ret);
+
+ r = proc_cmdline_get_key("systemd.hostname", 0, &hn);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to retrieve system hostname from kernel command line, ignoring: %m");
+ if (r == 0) /* not specified */
+ return -ENXIO;
+
+ /* The name may contain '?'/'$' wildcards (see hostname(5)). In the initrd the word lists (and
+ * possibly the machine ID) are typically not available yet so returning here means the default
+ * hostname will be used. Once the host file system is up the expansion succeeds and the intended
+ * name is applied. */
+ return validate_and_substitute_hostname_from_source(hn, "on the kernel command line", ret);
+}
+
int read_etc_hostname_stream(FILE *f, bool substitute_wildcards, char **ret) {
int r;
@@ -194,17 +229,9 @@ int hostname_setup(bool really) {
bool enoent = false;
int r;
- r = proc_cmdline_get_key("systemd.hostname", 0, &hn);
- if (r < 0)
- log_warning_errno(r, "Failed to retrieve system hostname from kernel command line, ignoring: %m");
- else if (r > 0) {
- if (hostname_is_valid(hn, VALID_HOSTNAME_TRAILING_DOT))
- source = HOSTNAME_TRANSIENT;
- else {
- log_warning("Hostname specified on kernel command line is invalid, ignoring: %s", hn);
- hn = mfree(hn);
- }
- }
+ r = acquire_hostname_from_cmdline(&hn);
+ if (r >= 0)
+ source = HOSTNAME_TRANSIENT;
if (!hn) {
r = read_etc_hostname(/* path= */ NULL, /* substitute_wildcards= */ true, &hn);
diff --git a/src/test/test-hostname-setup.c b/src/test/test-hostname-setup.c
index 0ac0acd475..e72bf1f16d 100644
--- a/src/test/test-hostname-setup.c
+++ b/src/test/test-hostname-setup.c
@@ -136,6 +136,33 @@ TEST(hostname_substitute_wildcards_words) {
ASSERT_OK(unsetenv("SYSTEMD_HOSTNAME_WORDLIST_PATH"));
}
+TEST(hostname_setup_cmdline_wildcards) {
+ _cleanup_(rm_rf_physical_and_freep) char *d = NULL;
+ int r;
+
+ r = sd_id128_get_machine(NULL);
+ if (ERRNO_IS_NEG_MACHINE_ID_UNSET(r))
+ return (void) log_tests_skipped_errno(r, "skipping cmdline wildcard hostname tests, no machine ID defined");
+
+ ASSERT_OK(mkdtemp_malloc("/tmp/hostname-wordlist.XXXXXX", &d));
+ ASSERT_OK(setenv("SYSTEMD_PROC_CMDLINE", "systemd.hostname=$-????", /* overwrite= */ true));
+ ASSERT_OK(setenv("SYSTEMD_HOSTNAME_WORDLIST_PATH", d, /* overwrite= */ true));
+
+ /* Word list missing (as e.g. in the initrd): the kernel command line hostname is ignored and the
+ * usual fallback logic applies, hostname_setup() must still succeed. */
+ ASSERT_OK(hostname_setup(/* really= */ false));
+
+ /* Word list present: hostname_setup() now exercises the cmdline expansion path and must still
+ * succeed. With really=false nothing is applied and the resolved name is not surfaced, so the
+ * concrete expansion is asserted separately in the hostname_substitute_wildcards test above. */
+ _cleanup_free_ char *one_list = ASSERT_PTR(path_join(d, "1"));
+ ASSERT_OK(write_string_file(one_list, "happy\nsad\n", WRITE_STRING_FILE_CREATE));
+ ASSERT_OK(hostname_setup(/* really= */ false));
+
+ ASSERT_OK(unsetenv("SYSTEMD_PROC_CMDLINE"));
+ ASSERT_OK(unsetenv("SYSTEMD_HOSTNAME_WORDLIST_PATH"));
+}
+
TEST(hostname_setup) {
hostname_setup(false);
}