From 493ac6425f47b583ea3b4774a8e95a3b1459f4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Thu, 6 Aug 2026 13:33:14 +0200 Subject: [PATCH 01/13] fix(switch-root): validate before ACK so failures return a real exit code --- src/api.c | 20 +++++++++++++++----- src/initctl.c | 14 +++++++------- src/initramfs.c | 37 +++++++++++++++++++++++++++++-------- src/private.h | 1 + 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/api.c b/src/api.c index b60a58d9..34772a8f 100644 --- a/src/api.c +++ b/src/api.c @@ -265,10 +265,8 @@ 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. + * Validate before ACK, so a bad request gets a real NACK instead + * of a false success. Parses "newroot\0newinit\0". */ static int do_switch_root_api(int sd, struct init_request *rq) { @@ -287,8 +285,20 @@ static int do_switch_root_api(int sd, struct init_request *rq) newinit = ptr; } + if (switch_root_precheck(newroot, newinit)) { + result = -1; + + rq->cmd = INIT_CMD_NACK; + snprintf(rq->data, sizeof(rq->data), "switch-root: %s", strerror(errno)); + if (write(sd, rq, sizeof(*rq)) != sizeof(*rq)) + dbg("Failed sending NACK to client"); + close(sd); + + return result; + } + /* - * 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; diff --git a/src/initctl.c b/src/initctl.c index f0dbcf86..3129a8e1 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -692,11 +692,11 @@ int do_switch_root(int argc, char *argv[]) printf(", init %s", newinit); 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)); + /* NACK is the only real failure case here, ACK/lost connection means success */ + if (client_send(&rq, sizeof(rq)) && rq.cmd == INIT_CMD_NACK) { + puts(rq.data); + return 1; + } return 0; } @@ -1414,9 +1414,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..f5b47f66 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -137,19 +137,14 @@ static int kill_cb(int pid, void *data) } /* - * 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. + * switch_root_precheck - Validate without side effects, so callers + * can reply to a bad request before committing to teardown. */ -int switch_root(const char *newroot, const char *newinit) +int switch_root_precheck(const char *newroot, const char *newinit) { struct stat newroot_st, oldroot_st; char init_path[PATH_MAX]; - int console_fd; int fd; - dev_t rootdev; - int signo; if (!newroot || !newroot[0]) { errno = EINVAL; @@ -208,6 +203,32 @@ int switch_root(const char *newroot, const char *newinit) return -1; } + return 0; +} + +/* + * Does not return on success - exec's the new init. + * On failure, returns -1 and sets errno. + */ +int switch_root(const char *newroot, const char *newinit) +{ + int console_fd; + dev_t rootdev; + int signo; + struct stat oldroot_st; + + if (switch_root_precheck(newroot, newinit)) + return -1; + + if (!newinit || !newinit[0]) + newinit = "/sbin/init"; + + /* Needed below for initramfs cleanup */ + if (stat("/", &oldroot_st)) { + logit(LOG_ERR, "switch_root: cannot stat /"); + return -1; + } + logit(LOG_NOTICE, "Performing switch_root to %s, init %s", newroot, newinit); /* Run switch_root hook before we start tearing things down */ diff --git a/src/private.h b/src/private.h index 1612803b..5825799d 100644 --- a/src/private.h +++ b/src/private.h @@ -60,6 +60,7 @@ 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); int switch_root (const char *newroot, const char *newinit); #endif /* FINIT_PRIVATE_H_ */ From d4a54eab1ebe30a17cc33c2949ad549224737648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Thu, 6 Aug 2026 17:11:35 +0200 Subject: [PATCH 02/13] fix(initramfs): validate path length and regular file in switch_root_precheck --- src/initramfs.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/initramfs.c b/src/initramfs.c index f5b47f66..613c8235 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -195,13 +195,22 @@ int switch_root_precheck(const char *newroot, const char *newinit) return -1; } - /* Verify init exists in new root */ - snprintf(init_path, sizeof(init_path), "%s%s", newroot, newinit); + /* 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)) { + logit(LOG_ERR, "switch_root: init path too long"); + errno = ENAMETOOLONG; + return -1; + } if (access(init_path, X_OK)) { logit(LOG_ERR, "switch_root: %s not found or not executable", init_path); errno = ENOENT; return -1; } + if (stat(init_path, &newroot_st) || !S_ISREG(newroot_st.st_mode)) { + logit(LOG_ERR, "switch_root: %s is not a regular file", init_path); + errno = EACCES; + return -1; + } return 0; } From 0fb46de5ec0b725c4172dc9fbdec52e4ba7c1168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Thu, 6 Aug 2026 17:19:15 +0200 Subject: [PATCH 03/13] fix(initramfs): abort switch_root if virtual fs move fails --- src/initramfs.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/initramfs.c b/src/initramfs.c index 613c8235..1b995dc5 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -119,7 +119,8 @@ static int do_move_mount(const char *oldpath, const char *newroot) 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; } @@ -270,10 +271,13 @@ int switch_root(const char *newroot, const char *newinit) /* Move virtual filesystems to new root */ dbg("Moving virtual filesystems..."); - do_move_mount("/dev", newroot); - do_move_mount("/proc", newroot); - do_move_mount("/sys", newroot); - do_move_mount("/run", newroot); + if (do_move_mount("/dev", newroot) || + do_move_mount("/proc", newroot) || + do_move_mount("/sys", newroot) || + do_move_mount("/run", newroot)) { + logit(LOG_ERR, "switch_root: failed to move virtual filesystems, aborting"); + return -1; + } /* Change to new root directory */ if (chdir(newroot)) { From 9e298d7f0736f2b6978c37a195646033aef5f57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 07:37:18 +0200 Subject: [PATCH 04/13] initramfs: split switch_root_precheck() out of switch_root() --- src/initramfs.c | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/initramfs.c b/src/initramfs.c index 1b995dc5..3c073aa0 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -119,8 +119,7 @@ static int do_move_mount(const char *oldpath, const char *newroot) makedir(newpath, 0755); if (mount(oldpath, newpath, NULL, MS_MOVE, NULL)) { - logit(LOG_ERR, "switch_root: failed to move %s to %s: %s", - oldpath, newpath, strerror(errno)); + dbg("Failed to move %s to %s: %s", oldpath, newpath, strerror(errno)); return -1; } @@ -196,29 +195,22 @@ int switch_root_precheck(const char *newroot, const char *newinit) return -1; } - /* 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)) { - logit(LOG_ERR, "switch_root: init path too long"); - errno = ENAMETOOLONG; - 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; } - if (stat(init_path, &newroot_st) || !S_ISREG(newroot_st.st_mode)) { - logit(LOG_ERR, "switch_root: %s is not a regular file", init_path); - errno = EACCES; - return -1; - } return 0; } /* - * Does not return on success - exec's the new init. - * On failure, returns -1 and sets errno. + * 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) { @@ -230,10 +222,11 @@ int switch_root(const char *newroot, const char *newinit) if (switch_root_precheck(newroot, newinit)) return -1; + /* Default to /sbin/init if not specified, same as switch_root_precheck() */ if (!newinit || !newinit[0]) newinit = "/sbin/init"; - /* Needed below for initramfs cleanup */ + /* Needed below for the initramfs cleanup */ if (stat("/", &oldroot_st)) { logit(LOG_ERR, "switch_root: cannot stat /"); return -1; @@ -271,13 +264,10 @@ int switch_root(const char *newroot, const char *newinit) /* Move virtual filesystems to new root */ dbg("Moving virtual filesystems..."); - if (do_move_mount("/dev", newroot) || - do_move_mount("/proc", newroot) || - do_move_mount("/sys", newroot) || - do_move_mount("/run", newroot)) { - logit(LOG_ERR, "switch_root: failed to move virtual filesystems, aborting"); - return -1; - } + do_move_mount("/dev", newroot); + do_move_mount("/proc", newroot); + do_move_mount("/sys", newroot); + do_move_mount("/run", newroot); /* Change to new root directory */ if (chdir(newroot)) { From 925d7f2b52ceac18055adf5e0c7e652e01ed3176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 07:42:06 +0200 Subject: [PATCH 05/13] initramfs: reject a non-regular init and an overlong init path --- src/initramfs.c | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/initramfs.c b/src/initramfs.c index 3c073aa0..13c954eb 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -142,7 +142,7 @@ static int kill_cb(int pid, void *data) */ int switch_root_precheck(const char *newroot, const char *newinit) { - struct stat newroot_st, oldroot_st; + struct stat newroot_st, oldroot_st, st; char init_path[PATH_MAX]; int fd; @@ -184,7 +184,7 @@ int switch_root_precheck(const char *newroot, const char *newinit) } if (fstat(fd, &oldroot_st)) { close(fd); - logit(LOG_ERR, "switch_root: cannot stat /"); + logit(LOG_ERR, "switch_root: cannot stat /: %s", strerror(errno)); return -1; } close(fd); @@ -195,13 +195,31 @@ int switch_root_precheck(const char *newroot, const char *newinit) return -1; } - /* Verify init exists in new root */ - snprintf(init_path, sizeof(init_path), "%s%s", newroot, newinit); + /* 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)) { + logit(LOG_ERR, "switch_root: init path too long"); + errno = ENAMETOOLONG; + return -1; + } if (access(init_path, X_OK)) { logit(LOG_ERR, "switch_root: %s not found or not executable", init_path); errno = ENOENT; return -1; } + if (stat(init_path, &st)) { + logit(LOG_ERR, "switch_root: cannot stat %s: %s", init_path, strerror(errno)); + return -1; + } + if (S_ISDIR(st.st_mode)) { + logit(LOG_ERR, "switch_root: %s is a directory, not init", init_path); + errno = EISDIR; + return -1; + } + if (!S_ISREG(st.st_mode)) { + logit(LOG_ERR, "switch_root: %s is not a regular file", init_path); + errno = ENOEXEC; + return -1; + } return 0; } @@ -228,7 +246,7 @@ int switch_root(const char *newroot, const char *newinit) /* Needed below for the initramfs cleanup */ if (stat("/", &oldroot_st)) { - logit(LOG_ERR, "switch_root: cannot stat /"); + logit(LOG_ERR, "switch_root: cannot stat /: %s", strerror(errno)); return -1; } From 581b49a7e59a8b48ee39c3cf528af1e44364d44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 07:49:01 +0200 Subject: [PATCH 06/13] initramfs: don't clobber errno between a failed check and its return --- src/initramfs.c | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/initramfs.c b/src/initramfs.c index 13c954eb..af731c5c 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -170,8 +170,11 @@ int switch_root_precheck(const char *newroot, const char *newinit) return -1; } if (fstat(fd, &newroot_st)) { + int saved_errno = errno; + close(fd); - logit(LOG_ERR, "switch_root: cannot stat %s", newroot); + logit(LOG_ERR, "switch_root: cannot stat %s: %s", newroot, strerror(saved_errno)); + errno = saved_errno; return -1; } close(fd); @@ -179,12 +182,18 @@ int switch_root_precheck(const char *newroot, const char *newinit) /* 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 /"); + int saved_errno = errno; + + logit(LOG_ERR, "switch_root: cannot open /: %s", strerror(saved_errno)); + errno = saved_errno; return -1; } if (fstat(fd, &oldroot_st)) { + int saved_errno = errno; + close(fd); - logit(LOG_ERR, "switch_root: cannot stat /: %s", strerror(errno)); + logit(LOG_ERR, "switch_root: cannot stat /: %s", strerror(saved_errno)); + errno = saved_errno; return -1; } close(fd); @@ -207,7 +216,10 @@ int switch_root_precheck(const char *newroot, const char *newinit) return -1; } if (stat(init_path, &st)) { - logit(LOG_ERR, "switch_root: cannot stat %s: %s", init_path, strerror(errno)); + int saved_errno = errno; + + logit(LOG_ERR, "switch_root: cannot stat %s: %s", init_path, strerror(saved_errno)); + errno = saved_errno; return -1; } if (S_ISDIR(st.st_mode)) { @@ -246,7 +258,10 @@ int switch_root(const char *newroot, const char *newinit) /* Needed below for the initramfs cleanup */ if (stat("/", &oldroot_st)) { - logit(LOG_ERR, "switch_root: cannot stat /: %s", strerror(errno)); + int saved_errno = errno; + + logit(LOG_ERR, "switch_root: cannot stat /: %s", strerror(saved_errno)); + errno = saved_errno; return -1; } From bd40b68edf09fee4c83716f047760017b459c8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 07:57:35 +0200 Subject: [PATCH 07/13] initramfs: give switch_root_precheck() a real error message --- src/initramfs.c | 113 +++++++++++++++++++++++++----------------------- src/private.h | 2 +- 2 files changed, 59 insertions(+), 56 deletions(-) diff --git a/src/initramfs.c b/src/initramfs.c index af731c5c..d2fe9480 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -137,45 +138,60 @@ static int kill_cb(int pid, void *data) } /* - * switch_root_precheck - Validate without side effects, so callers - * can reply to a bad request before committing to teardown. + * 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. */ -int switch_root_precheck(const char *newroot, const char *newinit) +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 + * + * 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_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]) { - 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: %s", newroot, strerror(saved_errno)); - errno = saved_errno; - return -1; + return switch_root_fail(errbuf, errbuflen, saved_errno, + "cannot stat %s: %s", newroot, strerror(saved_errno)); } close(fd); @@ -184,54 +200,40 @@ int switch_root_precheck(const char *newroot, const char *newinit) if (fd < 0) { int saved_errno = errno; - logit(LOG_ERR, "switch_root: cannot open /: %s", strerror(saved_errno)); - errno = saved_errno; - return -1; + 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 /: %s", strerror(saved_errno)); - errno = saved_errno; - 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)) { - logit(LOG_ERR, "switch_root: init path too long"); - errno = ENAMETOOLONG; - return -1; - } - if (access(init_path, X_OK)) { - logit(LOG_ERR, "switch_root: %s not found or not executable", init_path); - errno = ENOENT; - return -1; - } + 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; - logit(LOG_ERR, "switch_root: cannot stat %s: %s", init_path, strerror(saved_errno)); - errno = saved_errno; - return -1; - } - if (S_ISDIR(st.st_mode)) { - logit(LOG_ERR, "switch_root: %s is a directory, not init", init_path); - errno = EISDIR; - return -1; - } - if (!S_ISREG(st.st_mode)) { - logit(LOG_ERR, "switch_root: %s is not a regular file", init_path); - errno = ENOEXEC; - return -1; + 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); + + if (!S_ISREG(st.st_mode)) + return switch_root_fail(errbuf, errbuflen, ENOEXEC, "%s is not a regular file", init_path); return 0; } @@ -249,7 +251,8 @@ int switch_root(const char *newroot, const char *newinit) int signo; struct stat oldroot_st; - if (switch_root_precheck(newroot, newinit)) + /* 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() */ diff --git a/src/private.h b/src/private.h index 5825799d..8e87a181 100644 --- a/src/private.h +++ b/src/private.h @@ -60,7 +60,7 @@ 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); +int switch_root_precheck(const char *newroot, const char *newinit, char *errbuf, size_t errbuflen); int switch_root (const char *newroot, const char *newinit); #endif /* FINIT_PRIVATE_H_ */ From 5b1b920a9125f1424235871478a73fef0dcee002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 08:08:18 +0200 Subject: [PATCH 08/13] api: relay switch_root_precheck()'s message in the NACK --- src/api.c | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/api.c b/src/api.c index 34772a8f..ada480e6 100644 --- a/src/api.c +++ b/src/api.c @@ -265,11 +265,14 @@ static void bypass_shutdown(void *unused) } /* - * Validate before ACK, so a bad request gets a real NACK instead - * of a false success. Parses "newroot\0newinit\0". + * Handle switch_root API command. + * Parses data: "newroot\0newinit\0" + * 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; @@ -285,16 +288,10 @@ static int do_switch_root_api(int sd, struct init_request *rq) newinit = ptr; } - if (switch_root_precheck(newroot, newinit)) { - result = -1; - + if (switch_root_precheck(newroot, newinit, errbuf, sizeof(errbuf))) { rq->cmd = INIT_CMD_NACK; - snprintf(rq->data, sizeof(rq->data), "switch-root: %s", strerror(errno)); - if (write(sd, rq, sizeof(*rq)) != sizeof(*rq)) - dbg("Failed sending NACK to client"); - close(sd); - - return result; + snprintf(rq->data, sizeof(rq->data), "switch-root: %s", errbuf); + return -1; } /* @@ -582,8 +579,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"); From 170a51bf708e48f9a6d2a822ef4f99d4ff5fddd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 08:09:21 +0200 Subject: [PATCH 09/13] api: reject switch-root in the wrong runlevel with a real NACK --- src/api.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api.c b/src/api.c index ada480e6..aea70d46 100644 --- a/src/api.c +++ b/src/api.c @@ -474,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; From f8d7d02c8821522013e394458e9d9a372b54b24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 08:55:05 +0200 Subject: [PATCH 10/13] initctl: explain why switch-root ignores client_send() on its own --- src/initctl.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/initctl.c b/src/initctl.c index 3129a8e1..da87a034 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -692,7 +692,13 @@ int do_switch_root(int argc, char *argv[]) printf(", init %s", newinit); printf(" ...\n"); - /* NACK is the only real failure case here, ACK/lost connection means success */ + /* + * 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; From fd66886de363e924a900de068203fd388be3e4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 09:52:36 +0200 Subject: [PATCH 11/13] initramfs: don't treat an unmounted /run as a failed move --- src/initramfs.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/initramfs.c b/src/initramfs.c index d2fe9480..5f4d1e8f 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -109,11 +109,19 @@ 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 */ From c9ff0d31350875b51d9041c33fe0bbf224a7b3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 09:54:42 +0200 Subject: [PATCH 12/13] initramfs: abort switch_root if a virtual fs move fails --- src/initramfs.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/initramfs.c b/src/initramfs.c index 5f4d1e8f..51fa460e 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -128,7 +128,8 @@ static int do_move_mount(const char *oldpath, const char *newroot) 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; } @@ -257,6 +258,7 @@ 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 */ @@ -306,12 +308,20 @@ int switch_root(const char *newroot, const char *newinit) plugin_exit(); cond_exit(); - /* Move virtual filesystems to new root */ + /* + * 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_ERR, "switch_root: failed to move one or more virtual filesystems, aborting"); + return -1; + } /* Change to new root directory */ if (chdir(newroot)) { From 926656ad92f6d20b334655c25059c174f16dfe98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sobczak?= Date: Wed, 12 Aug 2026 10:06:36 +0200 Subject: [PATCH 13/13] finit: sulogin() instead of dying past point of no return --- src/finit.c | 2 +- src/initramfs.c | 59 +++++++++++++++++++++++++++++++++++-------------- src/private.h | 1 + 3 files changed, 45 insertions(+), 17 deletions(-) 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/initramfs.c b/src/initramfs.c index 51fa460e..f72316d7 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -308,6 +308,22 @@ int switch_root(const char *newroot, const char *newinit) plugin_exit(); cond_exit(); + /* + * 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 @@ -319,14 +335,18 @@ int switch_root(const char *newroot, const char *newinit) failed |= do_move_mount("/sys", newroot); failed |= do_move_mount("/run", newroot); if (failed) { - logit(LOG_ERR, "switch_root: failed to move one or more virtual filesystems, aborting"); - return -1; + 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 */ @@ -338,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, @@ -363,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 8e87a181..5b2339b0 100644 --- a/src/private.h +++ b/src/private.h @@ -62,6 +62,7 @@ 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_ */