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); }