Merge pull request #496 from FixeQD/master

switch-root: return real errors instead of false success

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-08-16 23:41:08 +02:00
committed by GitHub
5 changed files with 214 additions and 100 deletions
+22 -12
View File
@@ -236,14 +236,16 @@ static void bypass_shutdown(void *unused)
/*
* Handle switch_root API command.
* Parses data: "newroot\0newinit\0"
* Sends ACK before attempting switch_root since it doesn't return on success.
* Returns: result from switch_root() on failure, doesn't return on success.
*
* On precheck failure, returns 1 with a message in rq->data for
* api_cb() to NACK. Does not return on success, and a post-ACK
* failure has nobody left to reply to, so returns 0 either way.
*/
static int do_switch_root_api(int sd, struct init_request *rq)
{
char errbuf[128];
char *newroot, *newinit = NULL;
char *ptr;
int result;
dbg("switch-root %s", rq->data);
strterm(rq->data, sizeof(rq->data));
@@ -256,8 +258,13 @@ static int do_switch_root_api(int sd, struct init_request *rq)
newinit = ptr;
}
if (switch_root_precheck(newroot, newinit, errbuf, sizeof(errbuf))) {
snprintf(rq->data, sizeof(rq->data), "switch-root: %s", errbuf);
return 1;
}
/*
* Send ACK first, since we won't return from
* Send ACK now, since we won't return from
* switch_root() on success.
*/
rq->cmd = INIT_CMD_ACK;
@@ -265,12 +272,10 @@ static int do_switch_root_api(int sd, struct init_request *rq)
dbg("Failed sending ACK to client");
close(sd);
/* This does not return on success */
result = switch_root(newroot, newinit);
if (result)
logit(LOG_ERR, "switch_root failed: %s", strerror(errno));
/* Does not return on success, logs its own failures */
switch_root(newroot, newinit);
return result;
return 0;
}
static int do_reboot(int cmd, int timeout, char *buf, size_t len)
@@ -435,7 +440,10 @@ static void api_cb(uev_t *w, void *arg, int events)
case INIT_CMD_SWITCH_ROOT:
if (runlevel != INIT_LEVEL && runlevel != 1) {
warnx("switch-root only allowed in runlevel S or 1");
strlcpy(rq.data, "switch-root: only allowed in runlevel S or 1",
sizeof(rq.data));
warnx("%s", rq.data);
result = 1;
goto done;
}
break;
@@ -541,8 +549,10 @@ static void api_cb(uev_t *w, void *arg, int events)
break;
case INIT_CMD_SWITCH_ROOT:
do_switch_root_api(sd, &rq);
goto leave;
result = do_switch_root_api(sd, &rq);
if (result)
break; /* precheck failed before ACK, done: sends the NACK */
goto leave; /* ACK sent and sd closed by do_switch_root_api() */
case INIT_CMD_ACK:
dbg("Client failed reading ACK");
+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[] = {
+7 -3
View File
@@ -1161,10 +1161,14 @@ int do_switch_root(int argc, char *argv[])
printf(" ...\n");
/*
* On success, finit exec's new init and we lose connection.
* A "failure" to read reply is actually expected on success.
* Unlike do_cmd(), a failing client_send() is expected here: on
* success Finit execs the new init and the connection dies with
* no reply. Only an explicit NACK means the request was denied.
*/
client_send(&rq, sizeof(rq));
if (client_send(&rq, sizeof(rq)) && rq.cmd == INIT_CMD_NACK) {
puts(rq.data);
return 1;
}
return 0;
}
+181 -84
View File
@@ -27,6 +27,7 @@
#include <fcntl.h>
#include <ftw.h>
#include <limits.h>
#include <stdarg.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
@@ -105,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;
@@ -113,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;
}
@@ -136,6 +146,133 @@ static int kill_cb(int pid, void *data)
return 0;
}
/*
* Log a precheck failure, optionally handing the message back to the
* caller in errbuf for relaying to the client.
*/
static int __attribute__ ((format (printf, 4, 5)))
switch_root_fail(char *errbuf, size_t errbuflen, int err, const char *fmt, ...)
{
char msg[128];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
logit(LOG_ERR, "switch_root: %s", msg);
if (errbuf)
strlcpy(errbuf, msg, errbuflen);
errno = err;
return -1;
}
/*
* Validate a switch_root request without side effects, so the caller
* can reject a bad request before committing to teardown.
*
* On failure, returns -1 with errno set. If errbuf is non-NULL it
* also gets a text reason, better suited for a client reply than
* strerror(errno).
*/
int switch_root_precheck(const char *newroot, const char *newinit,
char *errbuf, size_t errbuflen)
{
struct stat newroot_st, oldroot_st, st;
char init_path[PATH_MAX];
int fd;
if (!newroot || !newroot[0])
return switch_root_fail(errbuf, errbuflen, EINVAL, "no new root given");
/* Default to /sbin/init if not specified */
if (!newinit || !newinit[0])
newinit = "/sbin/init";
/* Verify we're PID 1 */
if (getpid() != 1)
return switch_root_fail(errbuf, errbuflen, EPERM, "must be run as PID 1");
/* Verify newroot exists and is a directory */
fd = open(newroot, O_RDONLY | O_DIRECTORY);
if (fd < 0)
return switch_root_fail(errbuf, errbuflen, ENOTDIR,
"%s is not a directory", newroot);
if (fstat(fd, &newroot_st)) {
int saved_errno = errno;
close(fd);
return switch_root_fail(errbuf, errbuflen, saved_errno,
"cannot stat %s: %s", newroot, strerror(saved_errno));
}
close(fd);
/* Verify newroot is a mount point (different device than parent) */
fd = open("/", O_RDONLY | O_DIRECTORY);
if (fd < 0)
return switch_root_fail(errbuf, errbuflen, errno,
"cannot open /: %s", strerror(errno));
if (fstat(fd, &oldroot_st)) {
int saved_errno = errno;
close(fd);
return switch_root_fail(errbuf, errbuflen, saved_errno,
"cannot stat /: %s", strerror(saved_errno));
}
close(fd);
if (newroot_st.st_dev == oldroot_st.st_dev)
return switch_root_fail(errbuf, errbuflen, EINVAL,
"%s is not a mount point", newroot);
/* Verify init exists in new root and is a regular file */
if (paste(init_path, sizeof(init_path),
newroot, newinit) >= (int)sizeof(init_path))
return switch_root_fail(errbuf, errbuflen, ENAMETOOLONG,
"init path too long");
if (access(init_path, X_OK))
return switch_root_fail(errbuf, errbuflen, ENOENT,
"%s not found or not executable", init_path);
if (stat(init_path, &st))
return switch_root_fail(errbuf, errbuflen, errno,
"cannot stat %s: %s", init_path, strerror(errno));
if (S_ISDIR(st.st_mode))
return switch_root_fail(errbuf, errbuflen, EISDIR,
"%s is a directory, not init", init_path);
if (!S_ISREG(st.st_mode))
return switch_root_fail(errbuf, errbuflen, ENOEXEC,
"%s is not a regular file", init_path);
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
*
@@ -144,69 +281,25 @@ static int kill_cb(int pid, void *data)
*/
int switch_root(const char *newroot, const char *newinit)
{
struct stat newroot_st, oldroot_st;
char init_path[PATH_MAX];
struct stat oldroot_st;
int failed = 0;
int console_fd;
int fd;
dev_t rootdev;
int signo;
if (!newroot || !newroot[0]) {
errno = EINVAL;
/* No client left to relay a message to */
if (switch_root_precheck(newroot, newinit, NULL, 0))
return -1;
}
/* Default to /sbin/init if not specified */
/* Default to /sbin/init, as in switch_root_precheck() */
if (!newinit || !newinit[0])
newinit = "/sbin/init";
/* Verify we're PID 1 */
if (getpid() != 1) {
logit(LOG_ERR, "switch_root must be run as PID 1");
errno = EPERM;
return -1;
}
/* Verify newroot exists and is a directory */
fd = open(newroot, O_RDONLY | O_DIRECTORY);
if (fd < 0) {
logit(LOG_ERR, "switch_root: %s is not a directory", newroot);
errno = ENOTDIR;
return -1;
}
if (fstat(fd, &newroot_st)) {
close(fd);
logit(LOG_ERR, "switch_root: cannot stat %s", newroot);
return -1;
}
close(fd);
/* Verify newroot is a mount point (different device than parent) */
fd = open("/", O_RDONLY | O_DIRECTORY);
if (fd < 0) {
logit(LOG_ERR, "switch_root: cannot open /");
return -1;
}
if (fstat(fd, &oldroot_st)) {
close(fd);
logit(LOG_ERR, "switch_root: cannot stat /");
return -1;
}
close(fd);
if (newroot_st.st_dev == oldroot_st.st_dev) {
logit(LOG_ERR, "switch_root: %s is not a mount point", newroot);
errno = EINVAL;
return -1;
}
/* Verify init exists in new root */
snprintf(init_path, sizeof(init_path), "%s%s", newroot, newinit);
if (access(init_path, X_OK)) {
logit(LOG_ERR, "switch_root: %s not found or not executable", init_path);
errno = ENOENT;
return -1;
}
/* 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);
@@ -238,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. */
@@ -285,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));
}
/**
+3
View File
@@ -70,7 +70,10 @@ int plugin_init (uev_ctx_t *ctx);
void plugin_exit (void);
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_ */