diff --git a/src/api.c b/src/api.c index b60a58d9..aea70d46 100644 --- a/src/api.c +++ b/src/api.c @@ -267,11 +267,12 @@ 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 a precheck failure, sets rq->cmd = INIT_CMD_NACK with a message and returns -1 + * Returns: result from switch_root_precheck()/switch_root() on failure, doesn't return on success */ static int do_switch_root_api(int sd, struct init_request *rq) { + char errbuf[sizeof(rq->data)]; char *newroot, *newinit = NULL; char *ptr; int result; @@ -287,8 +288,14 @@ static int do_switch_root_api(int sd, struct init_request *rq) newinit = ptr; } + if (switch_root_precheck(newroot, newinit, errbuf, sizeof(errbuf))) { + rq->cmd = INIT_CMD_NACK; + 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; @@ -467,6 +474,7 @@ 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"); + result = 1; goto done; } break; @@ -572,8 +580,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 && rq.cmd == INIT_CMD_NACK) + break; /* precheck failed before ACK, done: sends the NACK */ + goto leave; /* success, or a post-ACK failure with sd already closed */ case INIT_CMD_ACK: dbg("Client failed reading ACK"); diff --git a/src/finit.c b/src/finit.c index 96061dd7..40c595d3 100644 --- a/src/finit.c +++ b/src/finit.c @@ -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[] = { diff --git a/src/initctl.c b/src/initctl.c index f0dbcf86..da87a034 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -693,10 +693,16 @@ 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. - */ - client_send(&rq, sizeof(rq)); + * Nonzero client_send() isn't failure here, unlike do_cmd(): + * success execs the new init and drops the connection. Only a real NACK means rejection + * + * Can't distinguish that from finit not running -- dead connect/write also returns nonzero with rq.cmd untouched + * Needs client_request() to signal whether anything was sent; every other caller here treats nonzero as failure + */ + if (client_send(&rq, sizeof(rq)) && rq.cmd == INIT_CMD_NACK) { + puts(rq.data); + return 1; + } return 0; } @@ -1414,9 +1420,9 @@ static int show_status(char *arg) snprintf(title, sizeof(title), "%-*s %-*s %-8s %-13s ", pw, "PID", iw, "IDENT", "STATUS", "RUNLEVELS"); if (!verbose) - strlcat(title, "DESCRIPTION", sizeof(title)); + strlcat(title, "DESCRIPTION", sizeof(title)); else - strlcat(title, "COMMAND", sizeof(title)); + strlcat(title, "COMMAND", sizeof(title)); print_header("%s", title); } diff --git a/src/initramfs.c b/src/initramfs.c index a9beb955..f72316d7 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -108,18 +109,27 @@ static int is_initramfs(void) static int do_move_mount(const char *oldpath, const char *newroot) { char newpath[PATH_MAX]; - struct stat st; + struct stat st, root_st; if (stat(oldpath, &st)) return 0; /* Not mounted, skip */ + /* stat() succeeding doesn't mean oldpath is a mount point, it succeeds on a plain directory too */ + + if (stat("/", &root_st)) + return 0; /* Can't tell, skip rather than risk a false fatal */ + + if (st.st_dev == root_st.st_dev) + 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; } @@ -137,74 +147,134 @@ static int kill_cb(int pid, void *data) } /* - * Perform switch_root to a new root filesystem + * switch_root_fail - Log a precheck failure and optionally hand the same message back to the caller via errbuf, + * so it can be relayed to a client instead of forcing it to reconstruct one from errno. + */ +static int 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 && errbuflen) + snprintf(errbuf, errbuflen, "%s", msg); + + errno = err; + return -1; +} + +/* + * switch_root_precheck - Validate without side effects, so callercan reply to a bad request before committing to teardown * - * This function does not return on success - it exec's the new init. - * On failure, it returns -1 and sets errno. + * On failure, returns -1 with errno set. If errbuf is non-NULL, it also gets a text reason, + * safe to show to a client instead of just strerror(errno) */ -int switch_root(const char *newroot, const char *newinit) +int switch_root_precheck(const char *newroot, const char *newinit, char *errbuf, size_t errbuflen) { - struct stat newroot_st, oldroot_st; + struct stat newroot_st, oldroot_st, st; char init_path[PATH_MAX]; - int console_fd; int fd; - dev_t rootdev; - int signo; - if (!newroot || !newroot[0]) { - errno = EINVAL; - return -1; - } + 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) { - logit(LOG_ERR, "switch_root must be run as PID 1"); - errno = EPERM; - return -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) { - logit(LOG_ERR, "switch_root: %s is not a directory", newroot); - errno = ENOTDIR; - return -1; - } + 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); - logit(LOG_ERR, "switch_root: cannot stat %s", newroot); - return -1; + 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) { - logit(LOG_ERR, "switch_root: cannot open /"); - return -1; + int saved_errno = errno; + + return switch_root_fail(errbuf, errbuflen, saved_errno, + "cannot open /: %s", strerror(saved_errno)); } if (fstat(fd, &oldroot_st)) { + int saved_errno = errno; + close(fd); - logit(LOG_ERR, "switch_root: cannot stat /"); - return -1; + 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) { - logit(LOG_ERR, "switch_root: %s is not a mount point", newroot); - errno = EINVAL; - return -1; + 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 (snprintf(init_path, sizeof(init_path), "%s%s", 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)) { + int saved_errno = errno; + + return switch_root_fail(errbuf, errbuflen, saved_errno, + "cannot stat %s: %s", init_path, strerror(saved_errno)); } + if (S_ISDIR(st.st_mode)) + return switch_root_fail(errbuf, errbuflen, EISDIR, "%s is a directory, not init", init_path); - /* 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; + if (!S_ISREG(st.st_mode)) + return switch_root_fail(errbuf, errbuflen, ENOEXEC, "%s is not a regular file", init_path); + + return 0; +} + +/* + * Perform switch_root to a new root filesystem + * + * This function does not return on success - it exec's the new init. + * On failure, it returns -1 and sets errno. + */ +int switch_root(const char *newroot, const char *newinit) +{ + int console_fd; + dev_t rootdev; + int signo; + int failed = 0; + struct stat oldroot_st; + + /* No client listening past this point, message doesn't matter */ + if (switch_root_precheck(newroot, newinit, NULL, 0)) + return -1; + + /* Default to /sbin/init if not specified, same as switch_root_precheck() */ + if (!newinit || !newinit[0]) + newinit = "/sbin/init"; + + /* Needed below for the initramfs cleanup */ + if (stat("/", &oldroot_st)) { + int saved_errno = errno; + + logit(LOG_ERR, "switch_root: cannot stat /: %s", strerror(saved_errno)); + errno = saved_errno; return -1; } @@ -238,17 +308,45 @@ int switch_root(const char *newroot, const char *newinit) plugin_exit(); cond_exit(); - /* Move virtual filesystems to new root */ + /* + * From here on we're past the point of no return: services + * are dead, plugins are gone, and there's nothing left to + * talk to us over the API socket. A failure from here on + * can't be handled by just returning -1 like the checks + * above, since there's no longer a live finit for that to + * mean anything to, so it drops into sulogin() instead: a + * maintenance shell and a reboot on exit, same as a fatal + * fsck() or fs_mount_all() failure gets at regular boot. + * + * Unblock signals now, before any of that, so a rescue shell + * started below doesn't inherit whatever mask we've been + * running with; it used to only happen right before execl(). + */ + 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 already logged inside do_move_mount(), this just tracks whether any of them happened + */ 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); + failed |= do_move_mount("/proc", newroot); + failed |= do_move_mount("/sys", newroot); + failed |= do_move_mount("/run", newroot); + if (failed) { + logit(LOG_CONSOLE | LOG_ALERT, "switch_root: failed to move one or more " + "virtual filesystems, attempting sulogin ..."); + sulogin(1); + return -1; /* not reached, sulogin(1) reboots */ + } /* Change to new root directory */ if (chdir(newroot)) { - err(1, "Failed to chdir to %s", newroot); - return -1; + logit(LOG_CONSOLE | LOG_ALERT, "switch_root: failed to chdir to %s: %s, " + "attempting sulogin ...", newroot, strerror(errno)); + sulogin(1); + return -1; /* not reached, sulogin(1) reboots */ } /* Delete contents of old root if we're on initramfs */ @@ -260,19 +358,25 @@ int switch_root(const char *newroot, const char *newinit) /* Mount --move newroot to / */ if (mount(newroot, "/", NULL, MS_MOVE, NULL)) { - err(1, "Failed to move %s to /", newroot); - return -1; + logit(LOG_CONSOLE | LOG_ALERT, "switch_root: failed to move %s to /: %s, " + "attempting sulogin ...", newroot, strerror(errno)); + sulogin(1); + return -1; /* not reached, sulogin(1) reboots */ } /* chroot to new root */ if (chroot(".")) { - err(1, "Failed to chroot to new root"); - return -1; + logit(LOG_CONSOLE | LOG_ALERT, "switch_root: failed to chroot to new root: %s, " + "attempting sulogin ...", strerror(errno)); + sulogin(1); + return -1; /* not reached, sulogin(1) reboots */ } if (chdir("/")) { - err(1, "Failed to chdir to /"); - return -1; + logit(LOG_CONSOLE | LOG_ALERT, "switch_root: failed to chdir to /: %s, " + "attempting sulogin ...", strerror(errno)); + sulogin(1); + return -1; /* not reached, sulogin(1) reboots */ } /* Reopen console in new root. dup2() closes the old fds itself, @@ -285,16 +389,17 @@ 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; + /* If we get here, exec failed: everything is already torn down, + * so this is the worst case, drop to a rescue shell rather than + * leave a dead PID 1 with nothing left to talk to it. */ + logit(LOG_CONSOLE | LOG_ALERT, "switch_root: failed to exec %s: %s, attempting sulogin ...", + newinit, strerror(errno)); + sulogin(1); + return -1; /* not reached, sulogin(1) reboots */ } /** diff --git a/src/private.h b/src/private.h index 1612803b..5b2339b0 100644 --- a/src/private.h +++ b/src/private.h @@ -60,7 +60,9 @@ 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_ */