initramfs: drop to sulogin when switch_root fails past the ACK

A failed move of /dev, /proc, /sys or /run is logged at dbg() level
and ignored, so the new init boots without its virtual filesystems
and falls over much later in some unrelated way.  The steps after,
chdir/mount/chroot and the final execl(), do return -1 on failure,
but by then all services are dead and the API socket is gone, so
there is nobody left to report to: the system hangs with a live but
useless PID 1.

Make a failed move fatal, and try all four moves even if one fails,
so a bad /dev does not also skip /proc, /sys and /run.  A plain
directory is not an error though: /run stays a plain directory on a
tmpfs rootfs, so only a path on a different device than / is treated
as a mount point and moved.

Any failure past the point of no return now drops to sulogin(1) for
a maintenance shell that reboots on exit, same as a fatal fsck() at
boot.  Signals are unblocked before the moves so the shell does not
inherit finit's blocked signal mask.

Signed-off-by: Paweł Sobczak <github@fixeq.qzz.io>
This commit is contained in:
Paweł Sobczak
2026-08-16 23:17:57 +02:00
committed by Joachim Wiberg
parent 7263641953
commit ad02b343f2
3 changed files with 70 additions and 32 deletions
+1 -1
View File
@@ -122,7 +122,7 @@ static void banner(void)
#endif
}
static int sulogin(int do_reboot)
int sulogin(int do_reboot)
{
int rc = EX_OSFILE;
char *cmd[] = {
+68 -31
View File
@@ -106,7 +106,7 @@ static int is_initramfs(void)
/*
* Move a mount point from oldpath to newpath under newroot
*/
static int do_move_mount(const char *oldpath, const char *newroot)
static int do_move_mount(const char *oldpath, const char *newroot, dev_t rootdev)
{
char newpath[PATH_MAX];
struct stat st;
@@ -114,13 +114,22 @@ static int do_move_mount(const char *oldpath, const char *newroot)
if (stat(oldpath, &st))
return 0; /* Not mounted, skip */
/*
* stat() succeeds on plain directories too, only a device
* differing from / marks a mount point. Cannot use fismnt()
* here since /proc may already have moved.
*/
if (st.st_dev == rootdev)
return 0; /* Not a mount point, skip */
snprintf(newpath, sizeof(newpath), "%s%s", newroot, oldpath);
/* Create target directory if needed */
makedir(newpath, 0755);
if (mount(oldpath, newpath, NULL, MS_MOVE, NULL)) {
dbg("Failed to move %s to %s: %s", oldpath, newpath, strerror(errno));
logit(LOG_ERR, "switch_root: failed to move %s to %s: %s",
oldpath, newpath, strerror(errno));
return -1;
}
@@ -242,6 +251,28 @@ int switch_root_precheck(const char *newroot, const char *newinit,
return 0;
}
/*
* Past the point of no return: services are dead and the API socket
* is gone, so failures cannot be reported back to anyone. Same deal
* as a fatal fsck() at boot: drop to a maintenance shell, sulogin(1)
* reboots when it exits.
*/
static int __attribute__ ((format (printf, 1, 2)))
switch_root_rescue(const char *fmt, ...)
{
char msg[128];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
logit(LOG_CONSOLE | LOG_ALERT, "switch_root: %s, attempting sulogin ...", msg);
sulogin(1);
return -1; /* not reached, sulogin(1) reboots */
}
/*
* Perform switch_root to a new root filesystem
*
@@ -251,6 +282,7 @@ int switch_root_precheck(const char *newroot, const char *newinit,
int switch_root(const char *newroot, const char *newinit)
{
struct stat oldroot_st;
int failed = 0;
int console_fd;
dev_t rootdev;
int signo;
@@ -263,10 +295,11 @@ int switch_root(const char *newroot, const char *newinit)
if (!newinit || !newinit[0])
newinit = "/sbin/init";
/* Needed below for the initramfs cleanup */
/* Needed below for the moves and the initramfs cleanup */
if (stat("/", &oldroot_st))
return switch_root_fail(NULL, 0, errno,
"cannot stat /: %s", strerror(errno));
rootdev = oldroot_st.st_dev;
logit(LOG_NOTICE, "Performing switch_root to %s, init %s", newroot, newinit);
@@ -298,42 +331,50 @@ int switch_root(const char *newroot, const char *newinit)
plugin_exit();
cond_exit();
/* Move virtual filesystems to new root */
/*
* Unblock signals already here, before the rescue paths in
* switch_root_rescue() can trigger, so a maintenance shell
* does not inherit our blocked signal mask.
*/
sig_unblock();
/*
* Move virtual filesystems to new root. Try all four even if
* one fails, so a bad /dev doesn't also skip /proc, /sys and
* /run. Each failure is logged by do_move_mount() itself.
*/
dbg("Moving virtual filesystems...");
do_move_mount("/dev", newroot);
do_move_mount("/proc", newroot);
do_move_mount("/sys", newroot);
do_move_mount("/run", newroot);
failed |= do_move_mount("/dev", newroot, rootdev);
failed |= do_move_mount("/proc", newroot, rootdev);
failed |= do_move_mount("/sys", newroot, rootdev);
failed |= do_move_mount("/run", newroot, rootdev);
if (failed)
return switch_root_rescue("failed to move virtual filesystems");
/* Change to new root directory */
if (chdir(newroot)) {
err(1, "Failed to chdir to %s", newroot);
return -1;
}
if (chdir(newroot))
return switch_root_rescue("failed to chdir to %s: %s",
newroot, strerror(errno));
/* Delete contents of old root if we're on initramfs */
rootdev = oldroot_st.st_dev;
if (is_initramfs()) {
dbg("Deleting initramfs contents...");
delete_initramfs_contents(rootdev, newroot);
}
/* Mount --move newroot to / */
if (mount(newroot, "/", NULL, MS_MOVE, NULL)) {
err(1, "Failed to move %s to /", newroot);
return -1;
}
if (mount(newroot, "/", NULL, MS_MOVE, NULL))
return switch_root_rescue("failed to move %s to /: %s",
newroot, strerror(errno));
/* chroot to new root */
if (chroot(".")) {
err(1, "Failed to chroot to new root");
return -1;
}
if (chroot("."))
return switch_root_rescue("failed to chroot to new root: %s",
strerror(errno));
if (chdir("/")) {
err(1, "Failed to chdir to /");
return -1;
}
if (chdir("/"))
return switch_root_rescue("failed to chdir to /: %s",
strerror(errno));
/* Reopen console in new root. dup2() closes the old fds itself,
* so open() returns a fd > STDERR_FILENO that we can always close. */
@@ -345,16 +386,12 @@ int switch_root(const char *newroot, const char *newinit)
close(console_fd);
}
/* Reset signals to default */
sig_unblock();
/* Exec the new init - this does not return on success */
dbg("Executing %s...", newinit);
execl(newinit, newinit, NULL);
/* If we get here, exec failed */
err(1, "Failed to exec %s", newinit);
return -1;
return switch_root_rescue("failed to exec %s: %s",
newinit, strerror(errno));
}
/**
+1
View File
@@ -73,6 +73,7 @@ void iterate_proc (int (*cb)(int, void *), void *data);
int switch_root_precheck(const char *newroot, const char *newinit,
char *errbuf, size_t errbuflen);
int switch_root (const char *newroot, const char *newinit);
int sulogin (int do_reboot);
#endif /* FINIT_PRIVATE_H_ */