mirror of
https://github.com/troglobit/finit.git
synced 2026-10-17 10:49:34 +00:00
Implement switch_root functionality allowing Finit to serve as the init
in an initramfs, then transition to the real root filesystem. Useful for systems requiring early boot tasks like LUKS unlock, LVM activation, or network boot before mounting the real root. Adds INIT_CMD_SWITCH_ROOT API command, `initctl switch-root` subcommand, and HOOK_SWITCH_ROOT plugin hook point. The implementation gracefully stops services, moves virtual filesystems (/dev, /proc, /sys, /run) to the new root, deletes initramfs contents to free memory, then execs the new init as PID 1. See GitHub Discussion #292 for background.
This commit is contained in:
1 parent
e6d3eb2526
commit
373738f3d1
15 files changed
+654
-1
No files matched your search
@@ -63,6 +63,7 @@ finit_SOURCES = api.c cgroup.c cgroup.h \
|
||||
exec.c finit.c finit.h \
|
||||
stty.c \
|
||||
helpers.c helpers.h \
|
||||
initramfs.c initramfs.h \
|
||||
iwatch.c iwatch.h \
|
||||
log.c log.h \
|
||||
mdadm.c mount.c \
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include "cond.h"
|
||||
#include "conf.h"
|
||||
#include "helpers.h"
|
||||
#include "initramfs.h"
|
||||
#include "log.h"
|
||||
#include "plugin.h"
|
||||
#include "private.h"
|
||||
@@ -245,6 +246,46 @@ static void bypass_shutdown(void *unused)
|
||||
do_shutdown(halt);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 do_switch_root() on failure, doesn't return on success.
|
||||
*/
|
||||
static int do_switch_root_api(int sd, struct init_request *rq)
|
||||
{
|
||||
char *newroot, *newinit = NULL;
|
||||
char *ptr;
|
||||
int result;
|
||||
|
||||
dbg("switch-root %s", rq->data);
|
||||
strterm(rq->data, sizeof(rq->data));
|
||||
|
||||
newroot = rq->data;
|
||||
ptr = strchr(newroot, '\0');
|
||||
if (ptr && ptr < rq->data + sizeof(rq->data) - 1) {
|
||||
ptr++;
|
||||
if (*ptr)
|
||||
newinit = ptr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Send ACK first, since we won't return from
|
||||
* do_switch_root() on success.
|
||||
*/
|
||||
rq->cmd = INIT_CMD_ACK;
|
||||
if (write(sd, rq, sizeof(*rq)) != sizeof(*rq))
|
||||
dbg("Failed sending ACK to client");
|
||||
close(sd);
|
||||
|
||||
/* This does not return on success */
|
||||
result = do_switch_root(newroot, newinit);
|
||||
if (result)
|
||||
logit(LOG_ERR, "switch_root failed: %s", strerror(errno));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static int do_reboot(int cmd, int timeout, char *buf, size_t len)
|
||||
{
|
||||
int rc = 1;
|
||||
@@ -403,6 +444,15 @@ static void api_cb(uev_t *w, void *arg, int events)
|
||||
rq.cmd, rq.data);
|
||||
goto leave;
|
||||
}
|
||||
break;
|
||||
|
||||
case INIT_CMD_SWITCH_ROOT:
|
||||
if (runlevel != INIT_LEVEL && runlevel != 1) {
|
||||
warnx("switch-root only allowed in runlevel S or 1");
|
||||
goto done;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -503,6 +553,10 @@ static void api_cb(uev_t *w, void *arg, int events)
|
||||
result = do_reboot(rq.cmd, rq.sleeptime, rq.data, sizeof(rq.data));
|
||||
break;
|
||||
|
||||
case INIT_CMD_SWITCH_ROOT:
|
||||
do_switch_root_api(sd, &rq);
|
||||
goto leave;
|
||||
|
||||
case INIT_CMD_ACK:
|
||||
dbg("Client failed reading ACK");
|
||||
goto leave;
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
#define INIT_CMD_HALT 21
|
||||
#define INIT_CMD_POWEROFF 22
|
||||
#define INIT_CMD_SUSPEND 23
|
||||
#define INIT_CMD_SWITCH_ROOT 24 /* Switch to new root filesystem */
|
||||
#define INIT_CMD_WDOG_HELLO 128 /* Watchdog register and hello */
|
||||
#define INIT_CMD_SVC_ITER 129
|
||||
#define INIT_CMD_SVC_QUERY 130
|
||||
|
||||
+56
-1
@@ -646,6 +646,59 @@ int do_halt (char *arg) { return do_cmd(INIT_CMD_HALT); }
|
||||
int do_poweroff(char *arg) { return do_cmd(INIT_CMD_POWEROFF); }
|
||||
int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); }
|
||||
|
||||
/**
|
||||
* do_switch_root - Switch to a new root filesystem (initramfs only)
|
||||
* @argc: Number of arguments (1 or 2)
|
||||
* @argv: [0] = newroot path, [1] = optional init path
|
||||
*
|
||||
* This command is only valid during runlevel S (bootstrap) or 1 when
|
||||
* running from an initramfs. It stops all services, moves virtual
|
||||
* filesystems, and exec's the new init.
|
||||
*/
|
||||
int do_switch_root(int argc, char *argv[])
|
||||
{
|
||||
struct init_request rq = {
|
||||
.magic = INIT_MAGIC,
|
||||
.cmd = INIT_CMD_SWITCH_ROOT,
|
||||
};
|
||||
char *newroot, *newinit = NULL;
|
||||
size_t off;
|
||||
|
||||
if (argc < 1)
|
||||
ERRX(2, "Usage: initctl switch-root NEWROOT [INIT]");
|
||||
|
||||
newroot = argv[0];
|
||||
if (argc > 1)
|
||||
newinit = argv[1];
|
||||
|
||||
/* Verify newroot exists */
|
||||
if (access(newroot, F_OK))
|
||||
ERR(1, "Cannot access %s", newroot);
|
||||
|
||||
/*
|
||||
* Pack both strings into rq.data with null separators:
|
||||
* "newroot\0newinit\0"
|
||||
*/
|
||||
strlcpy(rq.data, newroot, sizeof(rq.data));
|
||||
off = strlen(newroot) + 1;
|
||||
|
||||
if (newinit && off < sizeof(rq.data) - 1)
|
||||
strlcpy(rq.data + off, newinit, sizeof(rq.data) - off);
|
||||
|
||||
printf("Switching root to %s", newroot);
|
||||
if (newinit)
|
||||
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));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int utmp_show(char *file)
|
||||
{
|
||||
struct utmp *ut;
|
||||
@@ -1474,7 +1527,8 @@ static int usage(int rc)
|
||||
" reboot Reboot system\n"
|
||||
" halt Halt system\n"
|
||||
" poweroff Halt and power off system\n"
|
||||
" suspend Suspend system\n");
|
||||
" suspend Suspend system\n"
|
||||
" switch-root ROOT [INIT] Switch to new root filesystem (initramfs)\n");
|
||||
|
||||
if (utmp)
|
||||
fprintf(stderr,
|
||||
@@ -1654,6 +1708,7 @@ int main(int argc, char *argv[])
|
||||
{ "halt", NULL, do_halt, NULL, NULL },
|
||||
{ "poweroff", NULL, do_poweroff, NULL, NULL },
|
||||
{ "suspend", NULL, do_suspend, NULL, NULL },
|
||||
{ "switch-root", NULL, NULL, NULL, do_switch_root },
|
||||
|
||||
{ "utmp", NULL, do_utmp, &utmp, NULL },
|
||||
{ NULL, NULL, NULL, NULL, NULL }
|
||||
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
/* Initramfs switch_root support
|
||||
*
|
||||
* Copyright (c) 2008-2025 Joachim Wiberg <troglobit@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "config.h" /* Generated by configure script */
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <ftw.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/vfs.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <linux/magic.h>
|
||||
#ifdef _LIBITE_LITE
|
||||
# include <libite/lite.h>
|
||||
#else
|
||||
# include <lite/lite.h>
|
||||
#endif
|
||||
|
||||
#include "finit.h"
|
||||
#include "cond.h"
|
||||
#include "helpers.h"
|
||||
#include "initramfs.h"
|
||||
#include "log.h"
|
||||
#include "plugin.h"
|
||||
#include "private.h"
|
||||
#include "sig.h"
|
||||
#include "util.h"
|
||||
|
||||
/* State for delete_cb(), nftw() doesn't support passing user data */
|
||||
static const char *delete_skip;
|
||||
static dev_t delete_rootdev;
|
||||
|
||||
/*
|
||||
* nftw() callback to delete initramfs contents before switch_root.
|
||||
* Skips the new root mount point and stays on the same filesystem.
|
||||
*/
|
||||
static int delete_cb(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw)
|
||||
{
|
||||
(void)tflag;
|
||||
|
||||
/* Skip root directory itself */
|
||||
if (ftw->level == 0)
|
||||
return 0;
|
||||
|
||||
/* Skip the new root mount point */
|
||||
if (delete_skip && !strncmp(fpath, delete_skip, strlen(delete_skip)))
|
||||
return 0;
|
||||
|
||||
/* Stay on the same filesystem (initramfs) */
|
||||
if (sb->st_dev != delete_rootdev)
|
||||
return 0;
|
||||
|
||||
remove(fpath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Recursively delete initramfs contents to free memory before switch_root.
|
||||
*/
|
||||
static void delete_initramfs_contents(dev_t rootdev, const char *skip)
|
||||
{
|
||||
delete_rootdev = rootdev;
|
||||
delete_skip = skip;
|
||||
|
||||
nftw("/", delete_cb, 20, FTW_DEPTH | FTW_PHYS | FTW_MOUNT);
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if we're running on a tmpfs/ramfs (initramfs)
|
||||
*/
|
||||
static int is_initramfs(void)
|
||||
{
|
||||
struct statfs sfs;
|
||||
|
||||
if (statfs("/", &sfs))
|
||||
return 0;
|
||||
|
||||
return sfs.f_type == RAMFS_MAGIC || sfs.f_type == TMPFS_MAGIC;
|
||||
}
|
||||
|
||||
/*
|
||||
* Move a mount point from oldpath to newpath under newroot
|
||||
*/
|
||||
static int do_move_mount(const char *oldpath, const char *newroot)
|
||||
{
|
||||
char newpath[PATH_MAX];
|
||||
struct stat st;
|
||||
|
||||
if (stat(oldpath, &st))
|
||||
return 0; /* Not mounted, 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));
|
||||
return -1;
|
||||
}
|
||||
|
||||
dbg("Moved %s to %s", oldpath, newpath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int kill_cb(int pid, void *data)
|
||||
{
|
||||
int signo = *(int *)data;
|
||||
|
||||
kill(pid, signo);
|
||||
|
||||
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 do_switch_root(const char *newroot, const char *newinit)
|
||||
{
|
||||
struct stat newroot_st, oldroot_st;
|
||||
char init_path[PATH_MAX];
|
||||
int console_fd;
|
||||
dev_t rootdev;
|
||||
int signo;
|
||||
|
||||
if (!newroot || !newroot[0]) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Verify newroot exists and is a directory */
|
||||
if (stat(newroot, &newroot_st) || !S_ISDIR(newroot_st.st_mode)) {
|
||||
logit(LOG_ERR, "switch_root: %s is not a directory", newroot);
|
||||
errno = ENOTDIR;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Verify newroot is a mount point (different device than parent) */
|
||||
if (stat("/", &oldroot_st)) {
|
||||
logit(LOG_ERR, "switch_root: cannot stat /");
|
||||
return -1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
logit(LOG_NOTICE, "Performing switch_root to %s, init %s", newroot, newinit);
|
||||
|
||||
/* Run switch_root hook before we start tearing things down */
|
||||
plugin_run_hooks(HOOK_SWITCH_ROOT);
|
||||
|
||||
/* Stop all services gracefully */
|
||||
dbg("Stopping all services...");
|
||||
halt = SHUT_OFF; /* Prevent actual shutdown */
|
||||
|
||||
/* Use existing shutdown logic to stop services */
|
||||
api_exit();
|
||||
log_exit();
|
||||
plugin_run_hooks(HOOK_SHUTDOWN);
|
||||
|
||||
/* Kill remaining processes (except kernel threads and ourselves) */
|
||||
signo = SIGTERM;
|
||||
do_iterate_proc(kill_cb, &signo);
|
||||
do_usleep(500000); /* Give them 500ms */
|
||||
|
||||
signo = SIGKILL;
|
||||
do_iterate_proc(kill_cb, &signo);
|
||||
|
||||
/* Reap zombies */
|
||||
while (waitpid(-1, NULL, WNOHANG) > 0)
|
||||
;
|
||||
|
||||
/* Exit plugins */
|
||||
plugin_exit();
|
||||
cond_exit();
|
||||
|
||||
/* 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);
|
||||
|
||||
/* Change to new root directory */
|
||||
if (chdir(newroot)) {
|
||||
err(1, "Failed to chdir to %s", newroot);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* chroot to new root */
|
||||
if (chroot(".")) {
|
||||
err(1, "Failed to chroot to new root");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (chdir("/")) {
|
||||
err(1, "Failed to chdir to /");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Reopen console */
|
||||
close(STDIN_FILENO);
|
||||
close(STDOUT_FILENO);
|
||||
close(STDERR_FILENO);
|
||||
|
||||
console_fd = open("/dev/console", O_RDWR);
|
||||
if (console_fd >= 0) {
|
||||
dup2(console_fd, STDIN_FILENO);
|
||||
dup2(console_fd, STDOUT_FILENO);
|
||||
dup2(console_fd, STDERR_FILENO);
|
||||
if (console_fd > STDERR_FILENO)
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Local Variables:
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "linux"
|
||||
* End:
|
||||
*/
|
||||
@@ -0,0 +1,36 @@
|
||||
/* Initramfs switch_root support
|
||||
*
|
||||
* Copyright (c) 2008-2025 Joachim Wiberg <troglobit@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef FINIT_INITRAMFS_H_
|
||||
#define FINIT_INITRAMFS_H_
|
||||
|
||||
int do_switch_root(const char *newroot, const char *newinit);
|
||||
|
||||
#endif /* FINIT_INITRAMFS_H_ */
|
||||
|
||||
/**
|
||||
* Local Variables:
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "linux"
|
||||
* End:
|
||||
*/
|
||||
@@ -81,6 +81,9 @@
|
||||
CHOOSE(HOOK_SVC_RECONF, "nop"), \
|
||||
CHOOSE(HOOK_RUNLEVEL_CHANGE, "nop"), \
|
||||
\
|
||||
/* Switch root hook, before transitioning */ \
|
||||
CHOOSE(HOOK_SWITCH_ROOT, "hook/sys/switchroot"), \
|
||||
\
|
||||
/* Shutdown hooks, runlevel [06] */ \
|
||||
CHOOSE(HOOK_NETWORK_DN, "hook/net/down"), \
|
||||
CHOOSE(HOOK_SHUTDOWN, "hook/sys/shutdown"), \
|
||||
|
||||
@@ -59,6 +59,8 @@ void plugin_script_run(hook_point_t no);
|
||||
int plugin_init (uev_ctx_t *ctx);
|
||||
void plugin_exit (void);
|
||||
|
||||
void do_iterate_proc (int (*cb)(int, void *), void *data);
|
||||
|
||||
#endif /* FINIT_PRIVATE_H_ */
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user