From e6d3eb25267155a35a068c7e0f740e7d44af9e28 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Thu, 1 Jan 2026 16:02:36 -0500 Subject: [PATCH 1/3] Handle already-mounted cgroups in cgroup_init() Add handling for EBUSY when mounting cgroup2 filesystem, which occurs when cgroups are already mounted. This can happen after switch_root when cgroups were moved from the initramfs, or in container environments. Verify the existing mount is actually cgroup2 before proceeding, and track whether we mounted to avoid unmounting on error if we didn't. --- src/cgroup.c | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/cgroup.c b/src/cgroup.c index ccb44a77..20289cf4 100644 --- a/src/cgroup.c +++ b/src/cgroup.c @@ -34,6 +34,8 @@ #endif #include #include /* get_nprocs_conf() */ +#include +#include #include "cgroup.h" #include "finit.h" @@ -841,6 +843,7 @@ void cgroup_config(void) void cgroup_init(uev_ctx_t *ctx) { int opts = MS_NODEV | MS_NOEXEC | MS_NOSUID; + int mounted = 0; char buf[80]; FILE *fp; int fd; @@ -851,14 +854,36 @@ void cgroup_init(uev_ctx_t *ctx) #endif if (mount("none", FINIT_CGPATH, "cgroup2", opts, NULL)) { - if (errno == ENOENT) + if (errno == EBUSY) { + /* + * Already mounted - this happens after switch_root + * when cgroups were moved from the initramfs. + * Verify it's actually cgroup2 before proceeding. + */ + struct statfs sfs; + + if (statfs(FINIT_CGPATH, &sfs) || sfs.f_type != CGROUP2_SUPER_MAGIC) { + logit(LOG_ERR, "Mount point %s busy but not cgroup2", FINIT_CGPATH); + avail = 0; + return; + } + dbg("cgroup2 already mounted at %s, reusing", FINIT_CGPATH); + } else if (errno == ENOENT) { logit(LOG_INFO, "Kernel does not support cgroups v2, disabling."); - else if (errno == EPERM) /* Probably inside an unprivileged container */ + avail = 0; + return; + } else if (errno == EPERM) { + /* Probably inside an unprivileged container */ logit(LOG_INFO, "Not allowed to mount cgroups v2, disabling."); - else + avail = 0; + return; + } else { err(1, "Failed mounting cgroup v2"); - avail = 0; - return; + avail = 0; + return; + } + } else { + mounted = 1; } avail = 1; @@ -867,7 +892,8 @@ void cgroup_init(uev_ctx_t *ctx) if (!fp) { err(1, "Failed opening %s", FINIT_CGPATH "/cgroup.controllers"); abort: - umount(FINIT_CGPATH); + if (mounted) + umount(FINIT_CGPATH); avail = 0; return; } From 373738f3d15a928ab8b37dcc2a3ac47df68db824 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Thu, 1 Jan 2026 16:02:47 -0500 Subject: [PATCH 2/3] 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. --- doc/features.md | 23 ++++ doc/index.md | 1 + doc/initctl.md | 22 ++++ doc/plugins.md | 10 ++ doc/switchroot.md | 148 +++++++++++++++++++++++ mkdocs.yml | 1 + src/Makefile.am | 1 + src/api.c | 54 +++++++++ src/finit.h | 1 + src/initctl.c | 57 ++++++++- src/initramfs.c | 294 ++++++++++++++++++++++++++++++++++++++++++++++ src/initramfs.h | 36 ++++++ src/plugin.h | 3 + src/private.h | 2 + src/sig.c | 2 + 15 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 doc/switchroot.md create mode 100644 src/initramfs.c create mode 100644 src/initramfs.h diff --git a/doc/features.md b/doc/features.md index 51fea9e7..82e5fe78 100644 --- a/doc/features.md +++ b/doc/features.md @@ -288,4 +288,27 @@ Rescue mode can be disabled at build time with `configure --without-rescue`. See the [Rescue Mode](config/rescue.md) section for more information. +**Switch Root** + +Finit supports switching from an initramfs to a real root filesystem using +the built-in `initctl switch-root` command. This allows Finit to serve as +the init system in an initramfs for early boot tasks (LUKS unlock, LVM +activation, network boot) before transitioning to the real root. + +```bash +# In initramfs, after mounting the real root: +initctl switch-root /mnt/root +``` + +The switch-root operation: + +1. Runs the `HOOK_SWITCH_ROOT` hook for cleanup +2. Stops all services gracefully +3. Moves virtual filesystems (`/dev`, `/proc`, `/sys`, `/run`) to the new root +4. Deletes initramfs contents to free memory +5. Pivots to the new root and execs the new init + +See the [Switch Root](switchroot.md) section for complete documentation. + + [5]: https://en.wikipedia.org/wiki/Runlevel diff --git a/doc/index.md b/doc/index.md index 4e4f7481..f58b22ff 100644 --- a/doc/index.md +++ b/doc/index.md @@ -39,6 +39,7 @@ Features * [Cgroups v2](config/cgroups.md), both configuration and monitoring in [`initctl top`](initctl.md) * [Plugin support](plugins.md) for customization * Proper [rescue mode](config/rescue.md) with bundled `sulogin` for protected maintenance shell + * [Switch root](switchroot.md) support for initramfs-to-real-root transitions * Integration with [watchdogd][] for full system supervision * [Logging](config/logging.md) to kernel ring buffer before `syslogd` has started, see the recommended [sysklogd][] project for complete logging integration diff --git a/doc/initctl.md b/doc/initctl.md index aff7503d..de19725c 100644 --- a/doc/initctl.md +++ b/doc/initctl.md @@ -65,6 +65,7 @@ Commands: halt Halt system poweroff Halt and power off system suspend Suspend system + switch-root NEWROOT [INIT] Switch to new root filesystem (initramfs only) utmp show Raw dump of UTMP/WTMP db ``` @@ -143,3 +144,24 @@ Apr 8 12:37:46 alpine authpriv.info dropbear[2300]: Exit (root) from <192.168.1 Apr 8 15:02:11 alpine authpriv.info dropbear[2634]: Child connection from 192.168.121.1:48576 Apr 8 15:02:12 alpine authpriv.notice dropbear[2634]: Password auth succeeded for 'root' from 192.168.121.1:48576 ``` + + +Switch Root +----------- + +The `switch-root` command is used when running Finit in an initramfs to +transition to the real root filesystem. This is similar to the standalone +`switch_root(8)` utility but integrated into Finit. + +``` +initctl switch-root /mnt/root [/sbin/init] +``` + +Requirements: + +- Must be run during runlevel S (bootstrap) or runlevel 1 +- The new root must be a mount point (different device than /) +- Can only be used when Finit is running as PID 1 in an initramfs + +For complete documentation and usage examples, see the dedicated +[Switch Root](switchroot.md) section. diff --git a/doc/plugins.md b/doc/plugins.md index e2306310..73bdd9a3 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -205,6 +205,16 @@ hook points: new runlevel have been been stopped. When the hook has completed, Finit continues to start all services in the new runlevel. +### Switch Root Hooks + +* `HOOK_SWITCH_ROOT`, `hook/sys/switchroot`: Called when + `initctl switch-root` is issued, before the transition begins. Use + this hook to save state, unmount initramfs-only filesystems, or perform + cleanup before switching to the new root. Only runs when Finit is + operating as PID 1 in an initramfs. + + See the [Switch Root](switchroot.md) section for more information. + ### Shutdown Hooks * `HOOK_NETWORK_DN`, `hook/net/down`: Called right after having changed diff --git a/doc/switchroot.md b/doc/switchroot.md new file mode 100644 index 00000000..89ae08b6 --- /dev/null +++ b/doc/switchroot.md @@ -0,0 +1,148 @@ +Switch Root +=========== + +Finit supports switching from an initramfs to a real root filesystem +using the `initctl switch-root` command. This is useful for systems +that use an initramfs for early boot (LUKS, LVM, network boot, etc.) +and need to transition to the real root before starting services. + + +Usage +----- + +```sh +initctl switch-root NEWROOT [INIT] +``` + +- `NEWROOT`: Path to the mounted new root filesystem (e.g., `/mnt/root`) +- `INIT`: Optional path to init on the new root (default: `/sbin/init`) + + +Requirements +------------ + +1. Must be run during runlevel S (bootstrap) or runlevel 1 +2. `NEWROOT` must be a mount point (different device than /) +3. `INIT` must exist and be executable on the new root +4. Finit must be running as PID 1 (in initramfs) + + +How It Works +------------ + +1. Runs `HOOK_SWITCH_ROOT` for any cleanup scripts/plugins +2. Runs `HOOK_SHUTDOWN` to notify plugins +3. Stops all services and kills remaining processes +4. Exits all plugins gracefully +5. Moves `/dev`, `/proc`, `/sys`, `/run` to new root +6. Deletes initramfs contents (if on tmpfs/ramfs) to free memory +7. Moves new root mount to `/` +8. Chroots to new root +9. Reopens `/dev/console` for stdin/stdout/stderr +10. Execs new init as PID 1 + + +Example: Initramfs finit.conf +----------------------------- + +Configuration file `/etc/finit.conf` in the initramfs: + +``` +# /etc/finit.conf in initramfs + +# Mount the real root filesystem +run [S] name:mount-root /bin/mount /dev/sda1 /mnt/root -- Mounting root filesystem + +# Switch to real root after mount completes +run [S] name:switch-root /sbin/initctl switch-root /mnt/root -- Switching to real root +``` + +For more complex setups (LUKS, LVM, etc.): + +``` +# Unlock LUKS volume +run [S] name:cryptsetup /sbin/cryptsetup open /dev/sda2 cryptroot -- Unlocking encrypted root + +# Activate LVM +run [S] name:lvm /sbin/lvm vgchange -ay -- Activating LVM volumes + +# Mount root +run [S] name:mount-root /bin/mount /dev/vg0/root /mnt/root -- Mounting root + +# Switch root +run [S] name:switch-root /sbin/initctl switch-root /mnt/root -- Switching to real root +``` + + +Example: Using Runlevel 1 for Switch Root +----------------------------------------- + +For more complex initramfs setups where ordering of tasks becomes +difficult in runlevel S, you can perform the switch-root in runlevel 1: + +``` +# /etc/finit.conf in initramfs + +# Start mdevd for device handling +service [S] name:mdevd notify:s6 /sbin/mdevd -D %n -- Device event daemon +run [S] name:coldplug /sbin/mdevd-coldplug -- Coldplug devices + +# Mount the real root filesystem (after devices are ready) +run [S] name:mount-root /bin/mount /dev/sda1 /mnt/root -- Mounting root + +# Transition to runlevel 1 after all S tasks complete +# The switch-root runs cleanly in runlevel 1 +run [1] name:switch-root /sbin/initctl switch-root /mnt/root -- Switching to real root +``` + +This approach separates the initramfs setup (runlevel S) from the +switch-root operation (runlevel 1), making task ordering simpler. + + +Hooks +----- + +The `HOOK_SWITCH_ROOT` hook runs before the switch begins. Use it for: + +- Saving state to the new root +- Unmounting initramfs-only mounts +- Cleanup tasks + +Plugins can register for `HOOK_SWITCH_ROOT` just like other hooks: + +```c +static void my_switch_root_hook(void *arg) +{ + /* Cleanup before switch_root */ +} + +static plugin_t plugin = { + .name = "my-plugin", + .hook[HOOK_SWITCH_ROOT] = { + .cb = my_switch_root_hook + } +}; + +PLUGIN_INIT(plugin_init) +{ + plugin_register(&plugin); +} +``` + + +Conditions +---------- + +After switch_root, the new finit instance starts fresh. No conditions +or state are preserved across the switch. The new finit will: + +1. Re-read `/etc/finit.conf` from the new root +2. Re-initialize all conditions +3. Start services according to the new configuration + + +See Also +-------- + +- [switch_root(8)](https://man7.org/linux/man-pages/man8/switch_root.8.html) - util-linux switch_root utility +- [Kernel initramfs documentation](https://docs.kernel.org/filesystems/ramfs-rootfs-initramfs.html) diff --git a/mkdocs.yml b/mkdocs.yml index 8e937c08..8d02f735 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -79,6 +79,7 @@ nav: - Limitations: config/limitations.md - Usage: - Commands & Status: initctl.md + - Switch Root: switchroot.md - Rebooting & Halting: commands.md - Command Line Options: cmdline.md - Managing Services: service.md diff --git a/src/Makefile.am b/src/Makefile.am index 346c9542..835bc075 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 \ diff --git a/src/api.c b/src/api.c index 87e29fdb..cca415ea 100644 --- a/src/api.c +++ b/src/api.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; diff --git a/src/finit.h b/src/finit.h index 3c310dcd..5bef212b 100644 --- a/src/finit.h +++ b/src/finit.h @@ -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 diff --git a/src/initctl.c b/src/initctl.c index 7096d62e..25c52cf6 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -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 } diff --git a/src/initramfs.c b/src/initramfs.c new file mode 100644 index 00000000..0258bc4d --- /dev/null +++ b/src/initramfs.c @@ -0,0 +1,294 @@ +/* Initramfs switch_root support + * + * Copyright (c) 2008-2025 Joachim Wiberg + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _LIBITE_LITE +# include +#else +# include +#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: + */ diff --git a/src/initramfs.h b/src/initramfs.h new file mode 100644 index 00000000..33135e49 --- /dev/null +++ b/src/initramfs.h @@ -0,0 +1,36 @@ +/* Initramfs switch_root support + * + * Copyright (c) 2008-2025 Joachim Wiberg + * + * 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: + */ diff --git a/src/plugin.h b/src/plugin.h index c5febb43..87238270 100644 --- a/src/plugin.h +++ b/src/plugin.h @@ -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"), \ diff --git a/src/private.h b/src/private.h index f69ac0cd..15fbda57 100644 --- a/src/private.h +++ b/src/private.h @@ -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_ */ /** diff --git a/src/sig.c b/src/sig.c index 424b67dc..a8421fd1 100644 --- a/src/sig.c +++ b/src/sig.c @@ -77,7 +77,9 @@ #endif #include /* strerror() */ #include +#include #include +#include #include #ifdef _LIBITE_LITE # include From 8e7d1b7bb59aef943ab8d73de08a6882032526a3 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Fri, 2 Jan 2026 18:08:42 -0500 Subject: [PATCH 3/3] Refactor: drop do_ prefix from iterate_proc() and switch_root() The do_ prefix is conventionally reserved for local helper functions. Move switch_root() declaration to private.h alongside iterate_proc() and remove the now-empty initramfs.h header. --- src/Makefile.am | 2 +- src/api.c | 7 +++---- src/initramfs.c | 7 +++---- src/initramfs.h | 36 ------------------------------------ src/private.h | 3 ++- src/sig.c | 8 ++++---- 6 files changed, 13 insertions(+), 50 deletions(-) delete mode 100644 src/initramfs.h diff --git a/src/Makefile.am b/src/Makefile.am index 835bc075..bd126600 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -63,7 +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 \ + initramfs.c \ iwatch.c iwatch.h \ log.c log.h \ mdadm.c mount.c \ diff --git a/src/api.c b/src/api.c index cca415ea..381c2197 100644 --- a/src/api.c +++ b/src/api.c @@ -42,7 +42,6 @@ #include "cond.h" #include "conf.h" #include "helpers.h" -#include "initramfs.h" #include "log.h" #include "plugin.h" #include "private.h" @@ -250,7 +249,7 @@ 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 do_switch_root() on failure, doesn't return on success. + * Returns: result from switch_root() on failure, doesn't return on success. */ static int do_switch_root_api(int sd, struct init_request *rq) { @@ -271,7 +270,7 @@ static int do_switch_root_api(int sd, struct init_request *rq) /* * Send ACK first, since we won't return from - * do_switch_root() on success. + * switch_root() on success. */ rq->cmd = INIT_CMD_ACK; if (write(sd, rq, sizeof(*rq)) != sizeof(*rq)) @@ -279,7 +278,7 @@ static int do_switch_root_api(int sd, struct init_request *rq) close(sd); /* This does not return on success */ - result = do_switch_root(newroot, newinit); + result = switch_root(newroot, newinit); if (result) logit(LOG_ERR, "switch_root failed: %s", strerror(errno)); diff --git a/src/initramfs.c b/src/initramfs.c index 0258bc4d..59fff4ac 100644 --- a/src/initramfs.c +++ b/src/initramfs.c @@ -43,7 +43,6 @@ #include "finit.h" #include "cond.h" #include "helpers.h" -#include "initramfs.h" #include "log.h" #include "plugin.h" #include "private.h" @@ -142,7 +141,7 @@ static int kill_cb(int pid, void *data) * 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) +int switch_root(const char *newroot, const char *newinit) { struct stat newroot_st, oldroot_st; char init_path[PATH_MAX]; @@ -209,11 +208,11 @@ int do_switch_root(const char *newroot, const char *newinit) /* Kill remaining processes (except kernel threads and ourselves) */ signo = SIGTERM; - do_iterate_proc(kill_cb, &signo); + iterate_proc(kill_cb, &signo); do_usleep(500000); /* Give them 500ms */ signo = SIGKILL; - do_iterate_proc(kill_cb, &signo); + iterate_proc(kill_cb, &signo); /* Reap zombies */ while (waitpid(-1, NULL, WNOHANG) > 0) diff --git a/src/initramfs.h b/src/initramfs.h deleted file mode 100644 index 33135e49..00000000 --- a/src/initramfs.h +++ /dev/null @@ -1,36 +0,0 @@ -/* Initramfs switch_root support - * - * Copyright (c) 2008-2025 Joachim Wiberg - * - * 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: - */ diff --git a/src/private.h b/src/private.h index 15fbda57..1612803b 100644 --- a/src/private.h +++ b/src/private.h @@ -59,7 +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); +void iterate_proc (int (*cb)(int, void *), void *data); +int switch_root (const char *newroot, const char *newinit); #endif /* FINIT_PRIVATE_H_ */ diff --git a/src/sig.c b/src/sig.c index a8421fd1..e3e25b52 100644 --- a/src/sig.c +++ b/src/sig.c @@ -208,7 +208,7 @@ static void fs_swapoff(void) * * https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/ */ -void do_iterate_proc(int (*cb)(int, void *), void *data) +void iterate_proc(int (*cb)(int, void *), void *data) { DIR *dirp; @@ -290,7 +290,7 @@ static int do_wait(int secs) ; has_proc = 0; iterations--; - do_iterate_proc(status_cb, &has_proc); + iterate_proc(status_cb, &has_proc); } while (has_proc && iterations > 0); @@ -322,10 +322,10 @@ void do_shutdown(shutop_t op) * Tell remaining non-monitored processes to exit, give them * time to exit gracefully, 2 sec was customary, we go for 1. */ - do_iterate_proc(kill_cb, &signo); + iterate_proc(kill_cb, &signo); if (do_wait(1)) { signo = SIGKILL; - do_iterate_proc(kill_cb, &signo); + iterate_proc(kill_cb, &signo); } /* Exit plugins gracefully */