diff --git a/man/machine-id.xml b/man/machine-id.xml
index cf759f2a0e..7e889ca47a 100644
--- a/man/machine-id.xml
+++ b/man/machine-id.xml
@@ -82,10 +82,11 @@
For operating system images which are created once and used on multiple
machines, for example for containers or in the cloud,
- /etc/machine-id should be an empty file in the generic file
- system image. An ID will be generated during boot and saved to this file if
- possible. Having an empty file in place is useful because it allows a temporary file
- to be bind-mounted over the real file, in case the image is used read-only.
+ /etc/machine-id should be either missing or an empty file in the generic file
+ system image (the difference between the two options is described under "First Boot Semantics" below). An
+ ID will be generated during boot and saved to this file if possible. Having an empty file in place is
+ useful because it allows a temporary file to be bind-mounted over the real file, in case the image is
+ used read-only.
systemd-firstboot1
may be used to initialize /etc/machine-id on mounted (but not
@@ -115,6 +116,34 @@
early boot but become writable later on.
+
+ First Boot Semantics
+
+ /etc/machine-id is used to decide whether a boot is the first one. The rules
+ are as follows:
+
+
+ If /etc/machine-id does not exist, this is a first boot. During
+ early boot, systemd will write unitialized\n to this file and overmount
+ a temporary file which contains the actual machine ID. Later (after first-boot-complete.target
+ has been reached), the real machine ID will be written to disk.
+
+ If /etc/machine-id contains the string uninitialized,
+ a boot is also considered the first boot. The same mechanism as above applies.
+
+ If /etc/machine-id exists and is empty, a boot is
+ not considered the first boot. systemd will still bind-mount a file
+ containing the actual machine-id over it and later try to commit it to disk (if /etc/ is
+ writable).
+
+ If /etc/machine-id already contains a valid machine-id, this is
+ not a first boot.
+
+
+ If by any of the above rules, a first boot is detected, units with ConditionFirstBoot=yes
+ will be run.
+
+
Relation to OSF UUIDs
diff --git a/man/systemd.special.xml b/man/systemd.special.xml
index e43d765c9f..1434ead7d5 100644
--- a/man/systemd.special.xml
+++ b/man/systemd.special.xml
@@ -32,6 +32,7 @@
emergency.target,
exit.target,
final.target,
+ first-boot-complete.target,
getty.target,
getty-pre.target,
graphical.target,
@@ -878,6 +879,17 @@
stopped.
+
+ first-boot-complete.target
+
+ This passive target is intended as a synchronization point for units that need to run once
+ during the first boot. Only after all units ordered before this target have finished, will the
+ machine-id5
+ be committed to disk, marking the first boot as completed. If the boot is aborted at any time
+ before that, the next boot will re-run any units with ConditionFirstBoot=yes.
+
+
+
getty-pre.target
diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml
index 27035798bc..4b8e515fe2 100644
--- a/man/systemd.unit.xml
+++ b/man/systemd.unit.xml
@@ -1315,10 +1315,16 @@
ConditionFirstBoot=
Takes a boolean argument. This condition may be used to conditionalize units on
- whether the system is booting up with an unpopulated /etc/ directory
- (specifically: an /etc/ with no /etc/machine-id). This may
- be used to populate /etc/ on the first boot after factory reset, or when a new
- system instance boots up for the first time.
+ whether the system is booting up for the first time. This roughly means that /etc/
+ is unpopulated (for details, see "First Boot Semantics" in
+ machine-id5).
+ This may be used to populate /etc/ on the first boot after factory reset, or
+ when a new system instance boots up for the first time.
+
+ For robustness, units with ConditionFirstBoot=yes should order themselves
+ before first-boot-complete.target and pull in this passive target with
+ Wants=. This ensures that in a case of an aborted first boot, these units will
+ be re-run during the next system startup.
If the systemd.condition-first-boot= option is specified on the kernel
command line (taking a boolean), it will override the result of this condition check, taking
diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset
index dd268ae1b5..9e430904f1 100644
--- a/presets/90-systemd.preset
+++ b/presets/90-systemd.preset
@@ -37,6 +37,7 @@ disable systemd-networkd-wait-online.service
disable systemd-time-wait-sync.service
disable systemd-boot-check-no-failures.service
disable systemd-network-generator.service
+disable proc-sys-fs-binfmt_misc.mount
disable syslog.socket
diff --git a/src/core/machine-id-setup.c b/src/core/machine-id-setup.c
index 6aecd36fe6..c55e0b5321 100644
--- a/src/core/machine-id-setup.c
+++ b/src/core/machine-id-setup.c
@@ -11,6 +11,7 @@
#include "fd-util.h"
#include "fs-util.h"
#include "id128-util.h"
+#include "io-util.h"
#include "log.h"
#include "machine-id-setup.h"
#include "macro.h"
@@ -86,7 +87,7 @@ static int generate_machine_id(const char *root, sd_id128_t *ret) {
return 0;
}
-int machine_id_setup(const char *root, sd_id128_t machine_id, sd_id128_t *ret) {
+int machine_id_setup(const char *root, bool force_transient, sd_id128_t machine_id, sd_id128_t *ret) {
const char *etc_machine_id, *run_machine_id;
_cleanup_close_ int fd = -1;
bool writable;
@@ -143,13 +144,31 @@ int machine_id_setup(const char *root, sd_id128_t machine_id, sd_id128_t *ret) {
if (ftruncate(fd, 0) < 0)
return log_error_errno(errno, "Failed to truncate %s: %m", etc_machine_id);
- if (id128_write_fd(fd, ID128_PLAIN, machine_id, true) >= 0)
- goto finish;
+ /* If the caller requested a transient machine-id, write the string "uninitialized\n" to
+ * disk and overmount it with a transient file.
+ *
+ * Otherwise write the machine-id directly to disk. */
+ if (force_transient) {
+ r = loop_write(fd, "uninitialized\n", strlen("uninitialized\n"), false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write uninitialized %s: %m", etc_machine_id);
+
+ r = fsync_full(fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to sync %s: %m", etc_machine_id);
+ } else {
+ r = id128_write_fd(fd, ID128_PLAIN, machine_id, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write %s: %m", etc_machine_id);
+ else
+ goto finish;
+ }
}
fd = safe_close(fd);
- /* Hmm, we couldn't write it? So let's write it to /run/machine-id as a replacement */
+ /* Hmm, we couldn't or shouldn't write the machine-id to /etc?
+ * So let's write it to /run/machine-id as a replacement */
run_machine_id = prefix_roota(root, "/run/machine-id");
@@ -167,7 +186,7 @@ int machine_id_setup(const char *root, sd_id128_t machine_id, sd_id128_t *ret) {
return r;
}
- log_info("Installed transient %s file.", etc_machine_id);
+ log_full(force_transient ? LOG_DEBUG : LOG_INFO, "Installed transient %s file.", etc_machine_id);
/* Mark the mount read-only */
r = mount_follow_verbose(LOG_WARNING, NULL, etc_machine_id, NULL, MS_BIND|MS_RDONLY|MS_REMOUNT, NULL);
@@ -183,10 +202,22 @@ finish:
int machine_id_commit(const char *root) {
_cleanup_close_ int fd = -1, initial_mntns_fd = -1;
- const char *etc_machine_id;
+ const char *etc_machine_id, *sync_path;
sd_id128_t id;
int r;
+ /* Before doing anything, sync everything to ensure any changes by first-boot units are persisted.
+ *
+ * First, explicitly sync the file systems we care about and check if it worked. */
+ FOREACH_STRING(sync_path, "/etc/", "/var/") {
+ r = syncfs_path(AT_FDCWD, sync_path);
+ if (r < 0)
+ return log_error_errno(r, "Cannot sync %s: %m", sync_path);
+ }
+
+ /* Afterwards, sync() the rest too, but we can't check the return value for these. */
+ sync();
+
/* Replaces a tmpfs bind mount of /etc/machine-id by a proper file, atomically. For this, the umount is removed
* in a mount namespace, a new file is created at the right place. Afterwards the mount is also removed in the
* original mount namespace, thus revealing the file that was just created. */
diff --git a/src/core/machine-id-setup.h b/src/core/machine-id-setup.h
index d6ac62a882..e207ccf9c1 100644
--- a/src/core/machine-id-setup.h
+++ b/src/core/machine-id-setup.h
@@ -1,5 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
+#include
+
int machine_id_commit(const char *root);
-int machine_id_setup(const char *root, sd_id128_t requested, sd_id128_t *ret);
+int machine_id_setup(const char *root, bool force_transient, sd_id128_t requested, sd_id128_t *ret);
diff --git a/src/core/main.c b/src/core/main.c
index 6cd596fa79..728e1578b1 100644
--- a/src/core/main.c
+++ b/src/core/main.c
@@ -2001,15 +2001,26 @@ static void log_execution_mode(bool *ret_first_boot) {
*ret_first_boot = false;
log_info("Running in initial RAM disk.");
} else {
- /* Let's check whether we are in first boot, i.e. whether /etc is still unpopulated. We use
- * /etc/machine-id as flag file, for this: if it exists we assume /etc is populated, if it
- * doesn't it's unpopulated. This allows container managers and installers to provision a
- * couple of files already. If the container manager wants to provision the machine ID itself
- * it should pass $container_uuid to PID 1. */
+ int r;
+ _cleanup_free_ char *id_text = NULL;
- *ret_first_boot = access("/etc/machine-id", F_OK) < 0;
- if (*ret_first_boot)
- log_info("Running with unpopulated /etc.");
+ /* Let's check whether we are in first boot. We use /etc/machine-id as flag file
+ * for this: If it is missing or contains the value "uninitialized", this is the
+ * first boot. In any other case, it is not. This allows container managers and
+ * installers to provision a couple of files already. If the container manager
+ * wants to provision the machine ID itself it should pass $container_uuid to PID 1. */
+
+ r = read_one_line_file("/etc/machine-id", &id_text);
+ if (r < 0 || streq(id_text, "uninitialized")) {
+ if (r < 0 && r != -ENOENT)
+ log_warning_errno(r, "Unexpected error while reading /etc/machine-id, ignoring: %m");
+
+ *ret_first_boot = true;
+ log_info("Detected first boot.");
+ } else {
+ *ret_first_boot = false;
+ log_debug("Detected initialized system, this is not the first boot.");
+ }
}
} else {
if (DEBUG_LOGGING) {
@@ -2026,6 +2037,7 @@ static void log_execution_mode(bool *ret_first_boot) {
static int initialize_runtime(
bool skip_setup,
+ bool first_boot,
struct rlimit *saved_rlimit_nofile,
struct rlimit *saved_rlimit_memlock,
const char **ret_error_message) {
@@ -2059,7 +2071,8 @@ static int initialize_runtime(
status_welcome();
hostname_setup();
- machine_id_setup(NULL, arg_machine_id, NULL);
+ /* Force transient machine-id on first boot. */
+ machine_id_setup(NULL, first_boot, arg_machine_id, NULL);
(void) loopback_setup();
bump_unix_max_dgram_qlen();
bump_file_max_and_nr_open();
@@ -2787,6 +2800,7 @@ int main(int argc, char *argv[]) {
log_execution_mode(&first_boot);
r = initialize_runtime(skip_setup,
+ first_boot,
&saved_rlimit_nofile,
&saved_rlimit_memlock,
&error_message);
diff --git a/src/libsystemd/sd-id128/id128-util.c b/src/libsystemd/sd-id128/id128-util.c
index 335f22b920..ebbfb2d32e 100644
--- a/src/libsystemd/sd-id128/id128-util.c
+++ b/src/libsystemd/sd-id128/id128-util.c
@@ -10,6 +10,7 @@
#include "id128-util.h"
#include "io-util.h"
#include "stdio-util.h"
+#include "string-util.h"
char *id128_to_uuid_string(sd_id128_t id, char s[static ID128_UUID_STRING_MAX]) {
unsigned n, k = 0;
@@ -97,6 +98,11 @@ int id128_read_fd(int fd, Id128Format f, sd_id128_t *ret) {
switch (l) {
+ case 13:
+ case 14:
+ /* Treat an "uninitialized" id file like an empty one */
+ return f == ID128_PLAIN_OR_UNINIT && strneq(buffer, "uninitialized\n", l) ? -ENOMEDIUM : -EINVAL;
+
case 33: /* plain UUID with trailing newline */
if (buffer[32] != '\n')
return -EINVAL;
@@ -115,7 +121,7 @@ int id128_read_fd(int fd, Id128Format f, sd_id128_t *ret) {
_fallthrough_;
case 36: /* RFC UUID without trailing newline */
- if (f == ID128_PLAIN)
+ if (IN_SET(f, ID128_PLAIN, ID128_PLAIN_OR_UNINIT))
return -EINVAL;
buffer[36] = 0;
diff --git a/src/libsystemd/sd-id128/id128-util.h b/src/libsystemd/sd-id128/id128-util.h
index 1901bf119f..1453c00f2f 100644
--- a/src/libsystemd/sd-id128/id128-util.h
+++ b/src/libsystemd/sd-id128/id128-util.h
@@ -17,6 +17,10 @@ bool id128_is_valid(const char *s) _pure_;
typedef enum Id128Format {
ID128_ANY,
ID128_PLAIN, /* formatted as 32 hex chars as-is */
+ ID128_PLAIN_OR_UNINIT, /* formatted as 32 hex chars as-is; allow special "uninitialized"
+ * value when reading from file (id128_read() and id128_read_fd()).
+ *
+ * This format should be used when reading a machine-id file. */
ID128_UUID, /* formatted as 36 character uuid string */
_ID128_FORMAT_MAX,
} Id128Format;
diff --git a/src/machine-id-setup/machine-id-setup-main.c b/src/machine-id-setup/machine-id-setup-main.c
index 872b00c158..2bcd99109e 100644
--- a/src/machine-id-setup/machine-id-setup-main.c
+++ b/src/machine-id-setup/machine-id-setup-main.c
@@ -128,7 +128,7 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "Failed to read machine ID back: %m");
} else {
- r = machine_id_setup(arg_root, SD_ID128_NULL, &id);
+ r = machine_id_setup(arg_root, false, SD_ID128_NULL, &id);
if (r < 0)
return r;
}
diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index 82bb167397..5db08cb5b3 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -2726,7 +2726,7 @@ static int setup_machine_id(const char *directory) {
etc_machine_id = prefix_roota(directory, "/etc/machine-id");
- r = id128_read(etc_machine_id, ID128_PLAIN, &id);
+ r = id128_read(etc_machine_id, ID128_PLAIN_OR_UNINIT, &id);
if (r < 0) {
if (!IN_SET(r, -ENOENT, -ENOMEDIUM)) /* If the file is missing or empty, we don't mind */
return log_error_errno(r, "Failed to read machine ID from container image: %m");
diff --git a/src/partition/repart.c b/src/partition/repart.c
index d406c9dbe3..4cf6a5fe3a 100644
--- a/src/partition/repart.c
+++ b/src/partition/repart.c
@@ -3245,7 +3245,7 @@ static int context_read_seed(Context *context, const char *root) {
else if (fd < 0)
return log_error_errno(fd, "Failed to determine machine ID of image: %m");
else {
- r = id128_read_fd(fd, ID128_PLAIN, &context->seed);
+ r = id128_read_fd(fd, ID128_PLAIN_OR_UNINIT, &context->seed);
if (r == -ENOMEDIUM)
log_info("No machine ID set, using randomized partition UUIDs.");
else if (r < 0)
diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c
index 91120d7219..cbd3357219 100644
--- a/src/shared/dissect-image.c
+++ b/src/shared/dissect-image.c
@@ -2148,6 +2148,8 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
log_debug_errno(r, "Image contains invalid /etc/machine-id: %s", line);
} else if (r == 0)
log_debug("/etc/machine-id file is empty.");
+ else if (streq(line, "uninitialized"))
+ log_debug("/etc/machine-id file is uninitialized (likely aborted first boot).");
else
log_debug("/etc/machine-id has unexpected length %i.", r);
diff --git a/units/first-boot-complete.target b/units/first-boot-complete.target
new file mode 100644
index 0000000000..98b236e9ff
--- /dev/null
+++ b/units/first-boot-complete.target
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# 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=First Boot Complete
+Documentation=man:systemd.special(7)
+RefuseManualStart=yes
+ConditionFirstBoot=yes
diff --git a/units/meson.build b/units/meson.build
index e94e7f7efd..40ffca5058 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -17,6 +17,7 @@ units = [
['emergency.target', ''],
['exit.target', ''],
['final.target', ''],
+ ['first-boot-complete.target', ''],
['getty.target', '',
'multi-user.target.wants/'],
['getty-pre.target', ''],
diff --git a/units/systemd-firstboot.service b/units/systemd-firstboot.service
index 9f5c7101cd..e17fef7b36 100644
--- a/units/systemd-firstboot.service
+++ b/units/systemd-firstboot.service
@@ -13,7 +13,8 @@ Documentation=man:systemd-firstboot(1)
DefaultDependencies=no
Conflicts=shutdown.target
After=systemd-remount-fs.service
-Before=systemd-sysusers.service sysinit.target shutdown.target
+Before=systemd-sysusers.service sysinit.target first-boot-complete.target shutdown.target
+Wants=first-boot-complete.target
ConditionPathIsReadWrite=/etc
ConditionFirstBoot=yes
diff --git a/units/systemd-machine-id-commit.service b/units/systemd-machine-id-commit.service
index e3acb0f326..5616a35701 100644
--- a/units/systemd-machine-id-commit.service
+++ b/units/systemd-machine-id-commit.service
@@ -12,8 +12,8 @@ Description=Commit a transient machine-id on disk
Documentation=man:systemd-machine-id-commit.service(8)
DefaultDependencies=no
Conflicts=shutdown.target
-Before=sysinit.target shutdown.target
-After=local-fs.target
+Before=shutdown.target
+After=local-fs.target first-boot-complete.target
ConditionPathIsReadWrite=/etc
ConditionPathIsMountPoint=/etc/machine-id
diff --git a/units/systemd-random-seed.service.in b/units/systemd-random-seed.service.in
index f97f99a966..aa4b3e7357 100644
--- a/units/systemd-random-seed.service.in
+++ b/units/systemd-random-seed.service.in
@@ -14,7 +14,8 @@ DefaultDependencies=no
RequiresMountsFor=@RANDOM_SEED@
Conflicts=shutdown.target
After=systemd-remount-fs.service
-Before=shutdown.target
+Before=first-boot-complete.target shutdown.target
+Wants=first-boot-complete.target
ConditionVirtualization=!container
[Service]