Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions doc/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions doc/initctl.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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.
10 changes: 10 additions & 0 deletions doc/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
148 changes: 148 additions & 0 deletions doc/switchroot.md
Original file line number Diff line number Diff line change
@@ -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 <service/mdevd/ready> /sbin/mdevd-coldplug -- Coldplug devices

# Mount the real root filesystem (after devices are ready)
run [S] name:mount-root <run/coldplug/success> /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)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
iwatch.c iwatch.h \
log.c log.h \
mdadm.c mount.c \
Expand Down
53 changes: 53 additions & 0 deletions src/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,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 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
* 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 = 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;
Expand Down Expand Up @@ -403,6 +443,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;
}
Expand Down Expand Up @@ -503,6 +552,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;
Expand Down
38 changes: 32 additions & 6 deletions src/cgroup.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
#endif
#include <sys/mount.h>
#include <sys/sysinfo.h> /* get_nprocs_conf() */
#include <sys/vfs.h>
#include <linux/magic.h>

#include "cgroup.h"
#include "finit.h"
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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;
}
Expand Down
Loading