mirror of
https://github.com/troglobit/finit.git
synced 2026-10-07 00:00:23 +07:00
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:
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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_ */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user