Compare commits

..
112 Commits
Author SHA1 Message Date
Joachim Wiberg e74dff96ba Update ChangeLog and bump version for v4.17 GA
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-28 10:09:16 +02:00
Joachim Wiberg 9ed4407b20 Follow-up to 96f944b: fix regression in status output at boot
status() returns a pointer to a single static buffer, so calling it twice
in the same cprintf() argument list — status(3) and status(rc) — causes
one to overwrite the other before the format string is rendered.  When
status(3) wins, the line shows [ ⋯  ] instead of [ OK ].  Fix by copying
status(rc) into a local buffer before calling status(3).

Also drop the delline() calls added to print() — that macro writes \033[2K
to buffered stdout while cprintf() writes unbuffered to stderr, so the
erase sequences can arrive out of order.  The \r\e[K already present in
the cprintf format strings makes them redundant anyway.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-28 10:02:21 +02:00
Joachim Wiberg eb913e8bf2 initramfs: fix Coverity defects
Check return value of remove() in delete_cb() and log failures via
dbg(), CID 909395

Replace stat() calls with open(O_DIRECTORY)+ fstat() for newroot and "/"
checks.  Eliminates the check-then-use race and lets O_DIRECTORY do the
isdir validation atomically, CID 909394

Drop the explicit close(0/1/2) before opening /dev/console.  dup2()
closes the old targets itself, so open() returns a fd > STDERR_FILENO
that can always be closed unconditionally, removing the conditional
guard, CID 909393

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-26 22:11:19 +02:00
Joachim Wiberg 17d04779fb Update ChangeLog and bump version for v4.17-rc1
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-26 21:54:49 +02:00
Joachim Wiberg ab661423ba Merge pull request #488 from Ollie-Gutierrez/ctty-support
service: add optional controlling TTY via tty: stanza
2026-04-23 19:41:02 +02:00
Ollie Gutierrez 8e77ea68b2 service: add optional controlling TTY via tty: stanza 2026-04-23 15:08:57 +00:00
Joachim Wiberg b0e672bc9a Merge pull request #487 from aanderse/patch-1
tmpfiles.d: update /var/tmp mode
2026-04-18 07:34:37 +02:00
Aaron Andersen ad76e15949 tmpfiles.d: update /var/tmp mode
`/var/tmp` mode should match `/tmp`
2026-04-18 09:17:10 +09:00
Joachim Wiberg b8735cc893 Merge pull request #486 from aanderse/patch-1 2026-04-14 04:57:00 +02:00
Aaron Andersen c058b3f9ce plugins/plymouth.c: fixup email address 2026-04-14 09:03:19 +09:00
Joachim Wiberg 8f6c61606a Merge pull request #485 from finit-project/console-output-fixes
Atomic print and re-print desc on status, flush before reboot
2026-04-08 17:11:57 +02:00
Joachim Wiberg 96f944b816 Atomic print and re-print desc on status, flush before reboot
Refactor print() to emit description + final status in a single call to
cprintf(), preventing kernel messages from splitting the two parts.

For two-phase print(-1,...) + print_result() sequences used by, e.g.,
run_interactive, save the last description and re-print it before the
[ OK ] / [FAIL] output so the status is never left stranded on a blank
line when command output or kernel messages have scrolled away the
original description.

Finally, add print_exit() which drains the console output buffer with
tcdrain(2) and resets ANSI SGR attributes + cursor visibility before the
kernel takes back the console on reboot/halt, preventing escape code
leakage into bootloader or early-kernel output.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-07 11:36:19 +02:00
Joachim Wiberg a81530754b Merge pull request #483 from aanderse/plymouth
Add Plymouth boot splash plugin
2026-03-20 14:35:53 +01:00
Aaron Andersen 1518eb9466 Add Plymouth boot splash plugin
Manage the plymouthd lifecycle across boot, switch_root, and shutdown.
Activated by the "splash" kernel command line argument.
2026-03-20 09:18:18 -04:00
Joachim Wiberg 50b9e46e19 Merge pull request #484 from finit-project/rdep-stop-cond
Clear condition before stopping rdeps on reload
2026-03-19 06:53:40 +01:00
Joachim Wiberg c01faef99b service: clear condition before stopping rdeps on reload
When a service without SIGHUP reload support (noreload) is touched and
'initctl reload' is called, service_update_rdeps() correctly identifies
its reverse dependencies but only marks them dirty.  It does not clear
the service's condition, so when service_step_all() runs:

 - rdeps supporting SIGHUP hit the sm_in_reload() guard and break early,
   left running while their dependency is being killed.
 - rdeps without SIGHUP support may receive SIGTERM too late, after the
   dependency has already died and broken their connection, causing them
   to exit from RUNNING state and have their restart counter incremented.

Fix by calling cond_clear() on the service's condition immediately in
service_update_rdeps(), before service_step_all() runs.  cond_clear()
calls cond_update() which calls service_step() inline on all affected
services, which see COND_OFF and transition to STOPPING_STATE — all
before SIGTERM is ever sent to the dependency itself.

This mirrors the pattern already used in api.c:do_reload() for direct
'initctl reload <svc>' calls.

Fixes: avahi/mdns stop causing mdns-alias restart counter increment

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-03-19 06:37:50 +01:00
Joachim Wiberg 4867b7268b Merge pull request #482 from vazub/plugfix
Use explicit plugin names to prevent subtle macro processing bugs
2026-03-16 07:31:34 +01:00
bazub 662293e194 Use explicit plugin names to prevent subtle macro processing bugs 2026-03-06 20:36:23 +00:00
Joachim Wiberg 4ce810cc1f Merge pull request #481 from vazub/pathfix
Remove redundant global path var and fix memory corruption
2026-03-04 15:05:36 +01:00
bazub 184c079c08 Remove redundant global path var and fix memory corruption 2026-03-03 20:31:33 +00:00
Joachim Wiberg 17c6791c70 Update ChangeLog and bump version for v4.16 release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-27 10:35:32 +01:00
Joachim Wiberg 42f24f5af5 Silence shutdown logging by default
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-27 10:34:38 +01:00
Joachim Wiberg cef5049bf0 Update ChangeLog and bump version for v4.16-rc1
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-24 05:36:45 +01:00
Joachim Wiberg ec251f1ce1 Merge pull request #480 from aanderse/master 2026-02-24 05:05:05 +01:00
Aaron Andersen 98ec30d308 tmpfiles: add --exclude-prefix and -E flags
Add support for the --exclude-prefix=PATH option to skip rules whose
path starts with the specified prefix.  The option can be specified
multiple times to exclude multiple path prefixes.

The -E flag is a shortcut for:

    --exclude-prefix=/dev --exclude-prefix=/proc \
    --exclude-prefix=/run --exclude-prefix=/sys

This is useful to avoid creating files below virtual or memory-backed
file system mount points.
2026-02-23 15:39:44 +00:00
Joachim Wiberg e1fe870aaf Merge pull request #478 from mattiaswal/fix-initial-devmon
devmon: assert condition immediately if device already exists

  1. udev fires early, creates device nodes in /dev/                                                                                                           
  2. Config is parsed later, calling devmon_add_cond() for each dev/ condition
  3. At this point the device already exists but the inotify event was missed
  4. The PR's fexist() check catches this — new node is added to the TAILQ and condition is immediately asserted

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-23 08:09:12 +01:00
Mattias Walström f04fce544d devmon: assert condition immediately if device already exists
The config parsing happens after udev triggers the initial event,
make sure to assert the condition if the device node exists when adding
it from configuration.

Signed-off-by: Mattias Walström <lazzer@gmail.com>
2026-02-23 07:30:08 +01:00
Joachim Wiberg 7aa9ed5982 Merge pull request #474 from finit-project/multi-depend
Multi depend fix
2026-02-22 21:05:31 +01:00
Joachim Wiberg a1a92a04bd Update ChangeLog
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:51:07 +01:00
Joachim Wiberg 804f655d87 test: fix depserv.sh after regression in 0b182c06
A service that is reloaded should not trigger dependants to be reloaded
unless the new <~cond> is used.  Which is reserve for tightly coupled
services.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:36 +01:00
Joachim Wiberg 2c78e7429a Only remove managed pidfiles in service cleanup
For SysV services with pid:!/path, the pidfile belongs to the service
itself and Finit shouldn't delete it.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:36 +01:00
Joachim Wiberg d77f0127b4 Don't disrupt dependents on reload of SIGHUP-capable service
This fixes a real bug where `initctl reload syslogd` unconditionally
clears syslogd's pid condition, causing all dependent services (dbus,
dnsmasq, etc.) to be stopped even though syslogd handles SIGHUP
gracefully and its PID/pidfile persist.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:35 +01:00
Joachim Wiberg 4253d0de25 Run service stop: and reload: scripts as non-blocking processes
A reload: script, like 'frrinit.sh reload' could potentially take a
while to finish, during which Finit would be blocked.  This change
reuses the service_script_add(), used for ready: scripts, to track
these background helpers.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:35 +01:00
Joachim Wiberg d47181a4a7 Silence cgroup warnings for non-existing PIDs
When the kernel manages to reap a child process before we've moved it to
its proper cgroup it will return ESRCH (No such process), we can safely
ignore such errors for short-lived processes.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:34 +01:00
Joachim Wiberg 8642007d0e Debug shutdown hangs and guard with timer watchdog
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:34 +01:00
Joachim Wiberg 96c74ed48b Reformat signaling log messages for readability
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 15:51:25 +01:00
Joachim Wiberg 03a14bcaa5 Add <~cond> condition modifier for tightly coupled services
Conditions in Finit are dependencies: if A is asserted, service B is
allowed to run.  When A goes through FLUX (e.g., upstream reloads),
dependents are PAUSED and then simply resumed when the condition is
reasserted -- this is the correct behavior for barrier-style deps
like <pid/syslogd>.

However, some setups have tightly coupled services where dependents
must be reloaded/restarted when an upstream service reloads, not just
resumed.  E.g., the FRR routing stack on Infix OS:

    netd <pid/mgmtd> ← zebra <!pid/netd> ← {staticd,ripd} <!pid/zebra>

When netd reloads (SIGHUP), zebra and its dependents must be restarted
to pick up the new configuration.

The new '~' condition prefix marks a dependency as flux-sensitive:

    service <!~pid/netd> name:zebra ...

When the upstream condition goes FLUX and returns to ON, the dependent
is reloaded (SIGHUP) or restarted (noreload '!') instead of merely
resumed.  Transitivity follows naturally through the condition chain.

Closes #416
Closes #476

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 15:51:25 +01:00
Joachim Wiberg 4bb2c33859 Dependents not restarted after SIGHUP reload of service
When 'initctl reload' is called after marking a service in a dependency
chain dirty, Finit fails to restart (unfreeze) affected services.

This patch updates the pidfile plugin to watch for IN_ATTRIB changes,
e.g. when a process uses utimensat() to update its pidfile, and adds
service_step_all() at end of reload cycle to guarantee convergence
after conditions are reasserted.

Issue #476

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-17 07:08:30 +01:00
Joachim Wiberg ab81272083 test: new regression test to verify multi-chain deps
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-13 07:30:10 +01:00
Joachim Wiberg 1fbf03bc1e Clear pid condition on service collection to fix stale deps
In a setup like this, when 'netd' is marked dirty and subsequently is
reloaded, e.g., using 'initctl reload', zebra is properly restarted,
but staticd isn't:

mgmtd <!> ← netd <pid/mgmtd> ← zebra <!pid/netd> ← staticd <!pid/zebra>

Finit must invalidate the condition of zebra to trigger a restart also
of staticd.  This to guard against daemons like zebra that may fail to
clean up their pidfiles.

Fixes #475

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-13 07:30:00 +01:00
Joachim Wiberg 92a2861b1c Merge pull request #473 from mattiaswal/fix-devmon 2026-02-10 20:36:37 +01:00
Joachim Wiberg 922714780a Merge pull request #472 from finit-project/reload-foo 2026-02-10 20:33:50 +01:00
Mattias Walström b206c3d655 devmon: re-evaluate device conditions on reconf
Device conditions tracked by devmon were lost on `initctl reload`
because the reconf path did not re-assert them.  Add devmon_reconf()
to iterate all tracked device nodes and set or clear their conditions
based on current device presence.

Signed-off-by: Mattias Walström <lazzer@gmail.com>
2026-02-10 20:09:35 +01:00
Mattias Walström 109d8826bd devmon: Fix deletion of conditions
Only compare the beginning of the condition, not the whole
condition name.

Signed-off-by: Mattias Walström <lazzer@gmail.com>
2026-02-10 19:49:13 +01:00
Joachim Wiberg 60478106eb Update ChangeLog with latest changes and features
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 16:01:53 +01:00
Joachim Wiberg 0de9cfb020 doc: Note per-service reload behavior for conditions
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 16:01:47 +01:00
Joachim Wiberg 0b182c063e test: Extend depserv test with per-service reload, fix slay race
Verify that 'initctl reload foo' properly triggers dependent
services by checking that bar gets a new PID after the reload.
Also change the second test case from service/foo/running to
service/foo/ready which is the actual condition set by pidfile.so.

Fix a race in slay where the target process could exit between
the PID lookup and kill -9, causing spurious test failures in
tight kill loops (e.g., start-kill-service.sh).

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 16:00:57 +01:00
Joachim Wiberg f0914f6d32 Fix 'initctl reload NAME' not updating conditions for dependents
When reloading a specific service with 'initctl reload foo', the
pid/foo and service/foo/ready conditions were never cleared, so
dependent services were not notified of the reload.

Clear the service's pid condition and, for pid/none notify types,
the ready condition before reloading.  The conditions are then
reasserted by the pidfile inotify handler when the service touches
its PID file after processing SIGHUP.

For s6/systemd services the ready condition is left intact since
their readiness notification may not re-trigger on SIGHUP.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 15:59:49 +01:00
Joachim Wiberg 7090321ff3 Don't hide cursor when shutting down
Users starting Finit based systems using U-Boot or Barebox may otherwise
not get a visible cursor at their prompt.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 15:58:46 +01:00
Joachim Wiberg 266f1132a4 Merge pull request #471 from aanderse/remain-after-exit
Add remain:yes option for run/task oneshot commands

Fixes #457

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-08 08:37:25 +01:00
Aaron Andersen 5f7e8457af Add remain:yes option for run/task oneshot commands
Similar to systemd's RemainAfterExit=yes.  Prevents the task from
re-running on runlevel re-entry and ensures the post: script runs
when explicitly stopped or when leaving valid runlevels.

Useful for tasks that set up persistent state like firewall rules:

    task [2345] remain:yes \
         post:/usr/sbin/teardown-firewall \
         /usr/sbin/setup-firewall -- Firewall setup

Not supported for bootstrap-only tasks (runlevel S only) since these
are deleted immediately after completion.
2026-02-05 22:08:22 -05:00
Joachim Wiberg a215747355 Fix clone3 build failure with older toolchain kernel headers
Define __NR_clone3 (435) ourselves when not provided by the toolchain
headers.  The syscall number is stable kernel ABI and the same on all
architectures since Linux 5.3.

The existing runtime fallback to fork() handles older kernels that don't
support the syscall.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-29 13:11:23 +01:00
Joachim Wiberg dacda5ab5d Merge pull request #470 from aanderse/master 2026-01-20 07:18:46 +01:00
Aaron Andersen 39044adcf8 Create mount points in fs_init() if they don't exist
In containerized or virtualized environments, standard mount point
directories may not exist at boot.  Ensure they are created before
attempting to mount.
2026-01-19 15:20:43 -05:00
Joachim Wiberg 9396cd1d26 Merge pull request #469 from aanderse/tmpfiles
enhance tmpfiles implementation
2026-01-19 07:57:16 +01:00
Aaron Andersen c9fd4418e7 tmpfiles: fix f/F to apply ownership when writing content
When f or F types write content to a file, the mode and ownership
specified in the config should be applied. Previously, ownership was
only applied when create() was used (i.e., when no argument was
specified).

Now we explicitly apply mode and ownership after writing content to
the file.
2026-01-18 18:59:20 -05:00
Aaron Andersen 7206f745a2 tmpfiles: fix 'e' type to only adjust existing directories
According to tmpfiles.d(5), the 'e' type adjusts the mode and ownership
of existing paths but should not create them. Previously, mksubsys()
was used which could create directories.

Now we explicitly check if the path is an existing directory before
adjusting its permissions.
2026-01-18 18:59:20 -05:00
Aaron Andersen 25bcde12d4 tmpfiles: add support for numeric uid/gid in config files
Add parse_uid() and parse_gid() helper functions that support both
numeric IDs and name lookups. Update the d/D directory creation
handlers to use these new functions.

This allows config files to specify ownership using numeric UIDs and
GIDs instead of only usernames and group names, matching systemd-tmpfiles
behavior.
2026-01-18 18:59:20 -05:00
Aaron Andersen 7459ede806 tmpfiles: fix L+ to replace non-directory entries
The L+ type should replace existing entries with a symlink. Previously,
rmrf() was always called which is only appropriate for directories.
Now we check if the path is a directory first, and use erase() for
files and symlinks.
2026-01-18 18:59:20 -05:00
Aaron Andersen 6bf35513c3 tmpfiles: add support for config files on command line
Allow specifying one or more configuration files as command line
arguments instead of always processing all files in the standard
tmpfiles.d directories.

This enables targeted operations on specific config files:

    tmpfiles --create /etc/tmpfiles.d/myapp.conf
    tmpfiles --clean /tmp/test.conf /tmp/other.conf

When no config files are specified, the existing behavior of
processing all *.conf files in the standard directories is preserved.

Also refactors file processing into a helper function to reduce
code duplication.
2026-01-18 18:59:20 -05:00
Aaron Andersen 0b8d39329a tmpfiles: add --clean flag for age-based cleanup
Add support for the --clean (-C) flag to remove files and directories
older than the age specified in tmpfiles.d configuration entries.

The age field (6th column) in tmpfiles.d entries can now be used with
'd', 'D', and 'e' type entries to clean up old files.  Supported time
suffixes are: s (seconds), m (minutes), h (hours), d (days), w (weeks).

Example configuration:
    d /tmp/cache 0755 root root 10d

When run with --clean, files in /tmp/cache older than 10 days will be
removed.  The directory itself is preserved.

Uses a conservative cleanup approach matching systemd-tmpfiles:
 - Files: kept if ANY of atime, ctime, mtime is recent
 - Directories: kept if ANY of atime, mtime is recent (ctime excluded
   because cleanup itself updates directory ctime)

A value of "-" or "0" for age disables cleanup for that entry.

Note: x/X exclusion patterns are recognized but not yet implemented.
2026-01-18 18:59:20 -05:00
Aaron Andersen 84098dd8b7 tmpfiles: rename flags for clarity
Rename the command-line flag variables to be more descriptive:

  c_flag -> create_flag
  r_flag -> remove_flag

This improves code readability.
2026-01-18 18:59:20 -05:00
Joachim Wiberg c4463ec64f initctl: escape special characters in JSON output
Strings like command, description, and environment may contain characters
that need escaping for valid JSON, e.g., embedded quotes in command line
arguments like -V "NanoPi R2S".

Add json_escape() helper to handle quotes, backslashes, and control chars.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-18 23:26:37 +01:00
Joachim Wiberg 6f1808248e Merge pull request #468 from aanderse/master
netlink: enumerate existing interfaces at startup
2026-01-18 09:00:27 +01:00
Aaron Andersen 29029bb78f netlink: enumerate existing interfaces at startup
The netlink plugin only receives RTM_NEWLINK events for interfaces that
appear after the plugin starts.  Interfaces that already exist at boot
(e.g., virtio-net in QEMU) never generate events, so their conditions
like net/eth0/exist were never set.

Moving enumeration to PLUGIN_INIT doesn't work because it runs before
cond_init(), so the condition filesystem isn't ready yet.

Fix by registering an HOOK_SVC_PLUGIN callback that queries existing
interfaces and routes.  This hook runs during conf_init(), after the
condition system is initialized.
2026-01-14 15:58:22 -05:00
Joachim Wiberg bbe588ac16 Bump version for new release cycle and update ChangeLog
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-12 19:32:09 +01:00
Joachim Wiberg 34bf9a7776 Fix #467: TTY services stuck in restart state after non-zero exit
When a TTY exited with non-zero code (e.g., user with shell=/sbin/false),
it would enter restart state but never recover, requiring manual restart.

The throttling logic from commit f0032ab had two issues:

  1. Duplicate exit code check in service_retry() created infinite timer loop
  2. TTYs lacked default restart_tmo, causing timer to never start

Fix by removing duplicate check and ensuring TTYs get a 2-second default
restart_tmo for proper throttling.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-12 19:31:39 +01:00
Joachim Wiberg bb111f98fc Merge pull request #465 from aanderse/master
Set USER and LOGNAME environment variables when dropping privileges
2026-01-11 08:54:59 +01:00
Joachim Wiberg c7faf4f9e2 Merge pull request #466 from JackieMium/patch-1
Fix elogind path in elogind.conf
2026-01-11 08:53:29 +01:00
JackieMe 18774539c0 Fix elogind path in elogind.conf
On Debian and Debian-derived distro, the elogind is place at /usr/libexec/elogind
2026-01-11 00:00:06 -06:00
Aaron Andersen abaad560f0 Set USER and LOGNAME environment variables when dropping privileges
When a service is configured to run as a non-root user (@user), finit
correctly drops privileges via setuid() and sets HOME and PATH, but
does not set the USER and LOGNAME environment variables. They remain
set to "root" from boot time.

This causes problems for software that determines its identity from
the environment rather than getuid(). For example, rootless Podman
checks os.Getenv("USER") first when looking up subordinate UID/GID
ranges in /etc/subuid and /etc/subgid.

With USER=root but UID=1000, Podman looks up root's subuid entry
instead of the actual user's, causing applications like newuidmap
to fail. Setting USER and LOGNAME to match the actual user identity
follows POSIX conventions and matches the behavior of su, sudo, and
login.
2026-01-10 21:36:58 -05:00
Joachim Wiberg 0ef325d86c doc: improve cgroups documentation
- Grammar
 - Clarity
 - Examples

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-07 12:49:37 +01:00
Joachim Wiberg 2c94f4e45f doc: fix per-service cgroup syntax
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-07 12:49:13 +01:00
Joachim Wiberg da184e7922 doc: fix invalid user:group examples in cgroups.md
Introduced in 820b9a77 for v4.15.

Fixes #464

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-07 11:55:17 +01:00
Joachim Wiberg 1e93fc1671 Merge pull request #463 from aanderse/switch_root
Add switch_root support for initramfs to real root transitions
2026-01-03 00:49:07 +01:00
Aaron Andersen 8e7d1b7bb5 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.
2026-01-02 18:13:00 -05:00
Aaron Andersen 373738f3d1 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.
2026-01-01 19:10:24 -05:00
Aaron Andersen e6d3eb2526 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.
2026-01-01 16:02:36 -05:00
Joachim Wiberg d7fd5bc902 .github: ensure regression tests do not run in parallel
At least the sysvpart.sh regression test cannot run in parallel yet with
other tests (probably runparts.sh), so we must ensure the tests never
run in parallel, in particular at release.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 16:34:16 +01:00
Joachim Wiberg 3f98220d5d test: simplify
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:54:06 +01:00
Joachim Wiberg 0ca509a42e test: debug sysroot setup
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:47:11 +01:00
Joachim Wiberg d17144d16f .github: extract test results dir at release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:46:43 +01:00
Joachim Wiberg a608c5a5c9 .github: always upload test results, regardless
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:46:10 +01:00
Joachim Wiberg 9b44b99480 .github: remember to ldconfig
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:40:06 +01:00
Joachim Wiberg ad225156f1 Update ChangeLog and bump version for release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 11:09:16 +01:00
Joachim Wiberg eb92a915d3 initctl: drop logically dead code, found by Coverity Scan
The defines already check for plain mode.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 10:46:48 +01:00
Joachim Wiberg 69a4f2c115 Drop logically dead code, found by Coverity Scan
Checks for uid and gid introduced in d017661

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 10:42:41 +01:00
Joachim Wiberg 9ce95fe8c0 .github: fix logic in weekly workflow
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 09:51:02 +01:00
Joachim Wiberg 06889cc014 .github: verify system can find .so libs in /usr/local
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 09:48:33 +01:00
Joachim Wiberg 329f11b4da test: add barebones /etc/{passwd,group} and an ld.so.conf
Finit now requires being able to query at least for the root user and
group before starting any services.

Also, add support for using libraries installed in /usr/local

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 09:45:06 +01:00
Joachim Wiberg ce40e2b9d2 Rename tty services early from "init" -> "getty"
Finit has support for "Please press Enter to activate this console."
which means there's no getty yet running.  However, when profiling
systems with Finit, and embedded systems in general, a common metric
is the time from power-on to getty has started.

This commit makes sure to rename the process so that BusyBox pidof is
capable of detecting that "getty" has started.  This is mostly for the
bootchart2 project's bootchartd, the native BusyBox bootchartd does not
have this issue.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-31 23:34:04 +01:00
Joachim Wiberg d0176612c9 Default to user/group root for services and check for errors
This is a refactor of getuser() and getgroup() so that they always
return a valid user, and group, for all normal use-cases.  When an
error occurs we now handle it properly in service_fork() so as to
not attempt to start services with an invalid user/group setting
as root.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-31 23:29:28 +01:00
Joachim Wiberg 21753e26dd Set critical env PATH + SHELL early
When running Finit under boothcartd (bootchart2 project) the PATH is
lost due to a bug.  This was a wakeup, so set critical variables in
main() early, before calling fs_init().

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-31 23:29:28 +01:00
Joachim Wiberg 0dc2513b32 Follow-up to 702a606, too long string to hide cursor
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-31 23:29:27 +01:00
Joachim Wiberg d16bfa789b Follow-up to 7c8ab79, missing semicolon
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 15:22:09 +01:00
Joachim Wiberg 0b27778c96 .github: install missing glightbox plugin for mkdocs
This is for automatic image zoom.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 15:21:06 +01:00
Joachim Wiberg 3e25297e3c doc: major overhaul of distro recs, simplify & clarify
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 14:12:13 +01:00
Joachim Wiberg 6560968c8b doc: update feature list
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 13:54:22 +01:00
Joachim Wiberg c9a3c5c885 doc: reorg. of menu a bit
Try out navigation.tabs, if we don't like it we can revert.

Reorg for stricter sections, what information actually belongs where?
E.g., introduction is now in a Getting Started section.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 13:52:13 +01:00
Joachim Wiberg a2d45e8f5e doc: add missing link to setrlimit(2)
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 13:06:56 +01:00
Joachim Wiberg f6c69eb108 doc: follow-up to be2a0ec, clearify capabilities with @root
Also, add links to relevant man pages.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 13:06:07 +01:00
Joachim Wiberg f0032ab6b7 Throttle failing services, e.g., tty, on error exit code
Some 'respawn' type services, like gettys, may hog the CPU in error
states if the service immediately exits.  E.g., due to missing dev.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 12:32:01 +01:00
Joachim Wiberg 53c5d0e55a Always reset ownership and permissions on TTY device nodes
During /bin/login phase the TTY device node is chowned and chmodded to
the authenticated user.  It will remain in this state until the next
call to getty.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 12:12:18 +01:00
Joachim Wiberg 7c8ab7915f Fix #458: follow-up to be2a0ec, capabilities breaks root access
A user reports inability to re-start getty on the console after logging
out from a serial console.  After some digging it was found that the tty
was owned by the last user logged in and 600.  Even thougn getty runs as
root, it did not have permission to re-open the device node.

Turns out there was a minor bug in the new capability code that cleared
all capabilities from the root user.  A surprising amount of programs
worked just fine, but restarting getty gave it away.

The fix is to only call cap_setuid() when capabilities are set for the
service, otherwise we just fall back to setuid().

Also, refactor service_register() wrt. capabilities a bit so that we can
give users an early warning if the configuration is invalid, by adding a
parse_caps() helper function that calls cap_iab_from_text() to verify.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 00:49:48 +01:00
Joachim Wiberg 12f7855a78 Update ChangeLog for v4.15 release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 13:41:18 +01:00
Joachim Wiberg bcc3360b53 Update site_url for generated documentation
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 13:30:30 +01:00
Joachim Wiberg d3408d14a8 Simplify and update links
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 13:28:43 +01:00
Joachim Wiberg 804060444a .github: fix deploy issue
- We already have the SHA
 - Use 'git add -A' to handle deleted files too

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 13:16:45 +01:00
Joachim Wiberg de544a5b36 .github: deploy docs to project's pages
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 12:47:25 +01:00
Joachim Wiberg 0d48d1df49 Merge pull request #461 from aanderse/master
Add support for supplementary groups
2025-12-26 10:28:51 +01:00
Aaron Andersen b46592e818 Add support for supplementary groups
Implement supplementary group support for services, allowing them to
access resources owned by multiple groups. Uses the @user:group,sup1,sup2
syntax to explicitly specify supplementary groups, in addition to now
reading group membership from /etc/group.
2025-12-24 09:54:37 -05:00
Joachim Wiberg 22bc218c84 Fix #462: /dev/pts mounted with wrong mode
Before this fix:

    admin@infix:~$ sudo ls -la /dev/pts/
    total 0
    drwxr-xr-x    2 root     root             0 Dec 24 08:16 .
    drwxr-xr-x   13 root     root         13340 Dec 24 08:16 ..
    cr--------    1 root     tty       136,   0 Dec 24 08:18 0
    crw-rw-rw-    1 root     root        5,   2 Dec 24 08:16 ptmx
    admin@infix:~$ mount | grep devpts
    devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=400,ptmxmode=666)

After:

    admin@infix-00-00-00:~$ sudo ls -l /dev/pts/
    total 0
    crw--w----    1 root     tty       136,   0 Dec 24 08:21 0
    crw-rw-rw-    1 root     root        5,   2 Dec 24 08:20 ptmx
    admin@infix-00-00-00:~$ mount |grep pts
    devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-24 09:23:30 +01:00
79 changed files with 3021 additions and 369 deletions
+7 -6
View File
@@ -40,23 +40,23 @@ jobs:
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
tar xf libite-2.6.2.tar.gz
(cd libuev-2.4.1 && ./configure --prefix=/usr && make && sudo make install-strip)
(cd libite-2.6.2 && ./configure --prefix=/usr && make && sudo make install-strip)
(cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip)
(cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip)
sudo ldconfig
- uses: actions/checkout@v4
- name: Static Finit
run: |
./autogen.sh
./configure --prefix= --enable-static
make -j3 V=1
make -j9 V=1
- name: Regular Finit
run: |
./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \
--enable-x11-common-plugin --enable-testserv-plugin --with-watchdog \
--with-keventd --with-libsystemd \
CFLAGS="-fsanitize=address -ggdb"
make -j3 clean
make -j3 V=1
make -j9 clean
make -j9 V=1
- name: Install to /tmp
run: |
DESTDIR=/tmp make install-strip
@@ -78,8 +78,9 @@ jobs:
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
- name: Run Unit Tests
run: |
make check || (cat test/test-suite.log; false)
make -j1 check || (cat test/test-suite.log; false)
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: finit-test-${{ matrix.compiler }}
+2 -2
View File
@@ -60,8 +60,8 @@ jobs:
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
tar xf libite-2.6.2.tar.gz
(cd libuev-2.4.1 && ./configure --prefix=/usr && make && sudo make install-strip)
(cd libite-2.6.2 && ./configure --prefix=/usr && make && sudo make install-strip)
(cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip)
(cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip)
sudo ldconfig
- name: Configure
run: |
+47 -15
View File
@@ -19,12 +19,6 @@ on:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
@@ -46,23 +40,61 @@ jobs:
pipx inject mkdocs mkdocs-material
pipx inject mkdocs pymdown-extensions
pipx inject mkdocs mkdocs-callouts
pipx inject mkdocs mkdocs-glightbox
- name: Build documentation
run: mkdocs build
run: mkdocs build --clean
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
- name: Upload site artifact
uses: actions/upload-artifact@v4
with:
name: site
path: site/
deploy:
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
- name: Download site artifact
uses: actions/download-artifact@v4
with:
name: site
path: site/
- name: Checkout pages repo
uses: actions/checkout@v4
with:
repository: finit-project/finit-project.github.io
path: pages
ssh-key: ${{ secrets.DOCS_DEPLOY_KEY }}
persist-credentials: true
- name: Setup SSH for push
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
- name: Sync site to pages repo
run: |
rsync -a --delete --exclude .git site/ pages/
- name: Commit and push
run: |
cd pages/
if [ -z "$(git status --porcelain)" ]; then
exit 0
fi
SRC_REPO="${GITHUB_REPOSITORY}"
SRC_REF="${GITHUB_SHA::7}"
SRC_URL="https://github.com/${SRC_REPO}/commit/${SRC_REF}"
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add -A
git commit -m "docs: update from ${SRC_REPO}@${SRC_REF}" \
-m "For details, see ${SRC_URL}"
git push
+15 -5
View File
@@ -13,8 +13,6 @@ jobs:
name: Build and upload release tarball
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
env:
MAKEFLAGS: -j3
steps:
- uses: actions/checkout@v4
- name: Installing dependencies ...
@@ -25,8 +23,9 @@ jobs:
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
tar xf libite-2.6.2.tar.gz
(cd libuev-2.4.1 && ./configure --prefix=/usr && make && sudo make install-strip)
(cd libite-2.6.2 && ./configure --prefix=/usr && make && sudo make install-strip)
(cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip)
(cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip)
sudo ldconfig
- name: Setting release variables ...
id: build
run: |
@@ -54,10 +53,21 @@ jobs:
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
- name: Build release ...
run: |
make release || (cat test/test-suite.log; false)
# Get version for finding distcheck build directory
./configure --version | head -1
ver=$(./configure --version | head -1 | awk '{print $NF}')
dir=finit-$ver/_build/sub
make -j1 release || (cat $dir/test/test-suite.log; false)
ls -lF ../
mkdir -p artifacts/
mv ../*.tar.* artifacts/
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: finit-release-test-logs
path: finit-*/_build/sub/test/*.log
- name: Extract ChangeLog entry ...
run: |
awk '/-----*/{if (x == 1) exit; x=1;next}x' doc/ChangeLog.md \
+11 -4
View File
@@ -14,14 +14,14 @@ jobs:
schedule:
runs-on: ubuntu-latest
outputs:
run: ${{ steps.check.outputs.run }}
run: ${{ steps.schedule.outputs.run }}
steps:
- uses: actions/checkout@v4
- name: Check for any commits since last week
id: schedule
run: |
commits="$(git log --format=%H --since='1 week ago')"
if [ -n "$commits" ]; then
if [ -z "$commits" ]; then
echo "run=true" >> $GITHUB_OUTPUT
else
echo "run=false" >> $GITHUB_OUTPUT
@@ -39,8 +39,9 @@ jobs:
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
tar xf libite-2.6.2.tar.gz
(cd libuev-2.4.1 && ./configure --prefix=/usr && make && sudo make install-strip)
(cd libite-2.6.2 && ./configure --prefix=/usr && make && sudo make install-strip)
(cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip)
(cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip)
sudo ldconfig
- uses: actions/checkout@v4
- name: Base run
run: |
@@ -58,3 +59,9 @@ jobs:
ver=$(/tmp/sbin/initctl -V | awk '/Finit.*/ {print $2;}')
dir=finit-$ver/_build/sub
make -j1 distcheck || (cat $dir/test/test-suite.log; false)
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: finit-weekly-test-logs
path: finit-*/_build/sub/test/*.log
+12 -12
View File
@@ -1,17 +1,16 @@
[![License Badge][]][License] [![Release Badge][]][Release] [![GitHub Status][]][GitHub] [![Coverity Status][]][Coverity Scan]
<img align="right" src="doc/img/finit3.png" alt="Finit: Fast Init">
Finit is an alternative to [SysV init](https://en.wikipedia.org/wiki/Init)
and [systemd](https://www.freedesktop.org/wiki/Software/systemd/), focused
on small and embedded Linux systems, although fully usable on server and
[desktop systems][finix].
Finit is a fast, simple alternative to [SysV init](https://en.wikipedia.org/wiki/Init)
and [systemd](https://systemd.io), designed for small and embedded Linux systems.
It can also run on desktop and server systems, like [finix][].
> Reverse engineered from the [EeePC fastinit][]
> "gaps filled with frog DNA …"
> — [Claudio Matsuoka][]
For detailed information, explore our extensive documentation
:books: **<http://finit-project.github.io/finit/>**
:books: **<http://finit-project.github.io>**
<div align="center">
<img src="doc/img/alpine-screenshot2.png" alt="Alpine screenshot"><br/>
@@ -26,14 +25,14 @@ tutorials:
* :milky_way: [Void Linux](contrib/void/)
> [!NOTE]
> Support for various Linux distributions does not mean Finit installs
> easily on all architectures. Bundled install scripts are examples for
> standard installations, tested on amd64 (x86_64) systems. Custom
> setups, e.g., for embedded systems, can be found in the following
> [Buildroot][] based examples: [myLinux][], [Infix][], or the plain
> [br2-finit-demo](https://github.com/finit-project/br2-finit-demo).
> Finit can run on various Linux distributions, but the bundled install
> scripts are examples only. They have been tested on amd64 (x86_64) systems
> with standard configurations.
>
> For embedded systems, see these [Buildroot][]-based examples:
> [myLinux][], [Infix][], or [br2-finit-demo][].
[finix]: https://github.com/aanderse/finix
[finix]: https://github.com/finix-community/finix
[Buildroot]: https://buildroot.org
[Infix]: https://kernelkit.github.io
[myLinux]: https://github.com/troglobit/myLinux/
@@ -48,3 +47,4 @@ tutorials:
[GitHub Status]: https://github.com/finit-project/finit/actions/workflows/build.yml/badge.svg
[Coverity Scan]: https://scan.coverity.com/projects/3545
[Coverity Status]: https://scan.coverity.com/projects/3545/badge.svg
[br2-finit-demo]: https://github.com/finit-project/br2-finit-demo
+2 -1
View File
@@ -1,4 +1,4 @@
AC_INIT([Finit], [4.15-rc1], [https://github.com/finit-project/finit/issues],
AC_INIT([Finit], [4.17], [https://github.com/finit-project/finit/issues],
[finit], [https://http://finit-project.github.io/])
AC_CONFIG_AUX_DIR(aux)
AM_INIT_AUTOMAKE([1.11 foreign subdir-objects])
@@ -116,6 +116,7 @@ AC_PLUGIN([hotplug], [yes], [Start udevd or mdev kernel event datamon])
AC_PLUGIN([rtc], [yes], [Save and restore RTC using hwclock])
AC_PLUGIN([tty], [yes], [Automatically activate new TTYs, e.g. USB-to-serial])
AC_PLUGIN([urandom], [yes], [Setup and save random seed at boot/shutdown])
AC_PLUGIN([plymouth], [no], [Plymouth boot splash integration])
AC_PLUGIN([testserv], [no], [Test plugin to start test serv daemon])
# Check for extra arguments or packages
@@ -1,4 +1,4 @@
# On some systems, bootstrap can be really quick and cause elogind
# to not start before a user has managed to log in. To prevent
# this, add <pid/elogind> as condition to your TTYs in getty.conf
service [2345] <pid/syslogd> /lib/elogind/elogind -- Login manager
service [2345] <pid/syslogd> /usr/libexec/elogind -- Login manager
+122 -2
View File
@@ -3,9 +3,101 @@ Change Log
All relevant changes are documented in this file.
[4.15][UNRELEASED]
[4.17][] - 2026-04-28
---------------------
### Changes
- Add Plymouth boot splash plugin to manage the `plymouthd` lifecycle across boot,
`switch_root`, and shutdown. Activated by the `splash` kernel command line
argument. By Aaron Andersen
- Add `tty:` option for run/task/service stanzas to acquire a controlling
terminal. The device is opened as stdin, set as the session's controlling TTY
via `TIOCSCTTY`, and `TERM` is set automatically — `linux` for virtual terminals
and `vt102` for serial lines. By Ollie Gutierrez
- Improve console output atomicity at boot and shutdown: description and status
(`[ OK ]` / `[FAIL]`) are emitted in a single write so kernel messages cannot
split them. If output has been scrolled away, the description is re-printed
before the final status line. Console buffer is drained with `tcdrain(2)` and
ANSI attributes are reset before handing control back to the kernel at
shutdown/reboot
### Fixes
- Fix reverse-dependency condition handling on reload: when a service without
SIGHUP support is reloaded, its condition is now cleared immediately so
dependents transition to `STOPPING` before `SIGTERM` is sent to the parent,
preventing them from crashing with an incremented restart counter
- Fix `/var/tmp` mode in `tmpfiles.d/var.conf` from `0777` to `1777` (sticky bit)
to match `/tmp`, by Aaron Andersen
- Fix potential memory corruption in config parser: stale pointer captures of
`PATH` and `SHELL` at startup could dangle after environment modification;
`conf_reset_env()` now reads from the live environment instead, by Vasily Zubko
- Fix plugin name registration: replace `__FILE__` macro with explicit string
literals in all plugin structs and debug messages, preventing subtle build-
system–dependent macro expansion issues, by Vasily Zubko
[4.16][] - 2026-02-27
---------------------
### Changes
- Add `initctl switch-root` command and plugin hook point. Useful for
systems requiring early boot tasks like LUKS unlock, LVM activation,
or network boot before mounting the real root, by Aaron Andersen
- Set USER and LOGNAME environment variables when dropping privileges.
Fixes issues with software like rootless Podman that determines user
identity from environment variables, by Aaron Andersen
- Add `remain:yes` option for run/task oneshot commands, similar to the
systemd `RemainAfterExit=yes`, by Aaron Andersen
- Clear service conditions on `initctl reload NAME` to ensure dependent
services are properly updated
- Run service `stop:` and `reload:` scripts as non-blocking processes,
preventing Finit from stalling on long-running helper scripts
- Guard shutdown with timer watchdog to detect and debug shutdown hangs
- Add `~` condition prefix to propagate reload from a dependency to the
dependent service. E.g., `<!~pid/netd>` means not only a regular
condition, but when `netd` reloads, restart this service too. Similar
to systemd's directive `PropagatesReloadTo=`, but declared on the
consumer side. Issue #416
- `tmpfiles`: add support for `-E` and `--exclude-prefix` flags
contributed by Aaron Andersen
### Fixes
- Fix #464: invalid user:group examples in cgroups.md
- Fix #466: elogind path for Debian-based distros, by Jackie Liu
- Fix #467: TTY services stuck in restart state after non-zero exit.
Throttling logic introduced in v4.15 had duplicate checks causing
infinite timer loop, and TTYs lacked default restart timeout
- Fix #475: clear pid condition on service collection to fix stale
deps. When a service crashes (SIGKILL), the RUNNING → HALTED path
bypasses STOPPING where `cond_clear()` is normally called, leaving
dependents stuck
- Fix #476: dependents not restarted after SIGHUP reload of service in
dependency chain. Add `service_step_all()` at end of reload cycle to
guarantee convergence after conditions are reasserted. See also the
new `~` condition prefix (above) to propagate reload to dependents
- Fix #478: fix `<dev/foo>` conditions at boot, conditions that exist
before inotify was set up. Found and fixed by Mattias Walström
- Fix reload of SIGHUP-capable services incorrectly disrupting their
dependents. E.g., `initctl reload syslogd` would stop dbus, dnsmasq,
etc. even though syslogd handles SIGHUP gracefully and its PID persists
- Only remove managed pidfiles in service cleanup. For SysV services
with `pid:!/path`, the pidfile belongs to the service itself and Finit
should not delete it
- Silence spurious cgroup warnings for short-lived processes where the
kernel reaps the child before cgroup assignment completes
- Fix handling of already-mounted cgroups in `cgroup_init()`, can occur
after switch_root or in container environments
- Improve cgroups documentation clarity, grammar, and examples
[4.15][] - 2026-01-01
---------------------
The Finit project now has a new home page: <https://finit-project.github.io>
### Changes
- Change default reboot to SOC reset from WDT reset, issue #460
- Bump required versions of libite (v2.6.2) and libuev (v2.4.1)
@@ -24,17 +116,43 @@ All relevant changes are documented in this file.
descriptive names like `/system/udevd/` instead of filename derived names
like `/system/10-hotplug/`. Syntax supports combining with other options:
`cgroup.system,name:foo,delegate,cpu.max:10000`
- Add support for Linux capabilities, allowing services to run with minimal
required privileges instead of running as root. Uses the modern IAB API
(Inheritable, Ambient, Bounding) from libcap. By Aaron Andersen
- Add support for supplementary groups for services. Uses the syntax
`@user:group,sup1,sup2` to explicitly specify supplementary groups, in
addition to reading group membership from `/etc/group`. By Aaron Andersen
- Add CPU throttled information to `initctl` output, showing when a service
has been throttled due to reaching its CPU quota
- Add memory field to `initctl --json` status output for cgroup-enabled
services
- Default to user and group `root` (UID 0) for services
- Always check that service user and group resolve to valid UID/GID pairs,
otherwise log error and refuse to start service
- Hide cursor at boot and shutdown for cleaner visual experience
- Rename `tty` services early from `init` -> `getty` even though no getty
process has been started yet. This to aid in boot timing detection using
tools like `bootchartd` (BusyBox version or bootchart2 project) when Finit
only shows *"Please press Enter to activate this console."*
- Services with `respawn` option (including TTYs) now delay retry on non-zero
exit codes instead of immediate respawn. Prevents busy-loop when service
fails repeatedly, which on some systems may have a crippling effect
### Fixes
- Fix #453: RTC plugin fixes by Ming Liu, Atlas Copco
- Fix #455: ANSI escape sequences logged by tools using glib logging,
caused by use of PTY instead of pipe for stdout/stderr redirection
- Fix #462: `/dev/pts` mounted with wrong mode (400 instead of 620), causing
permission issues with pseudo-terminals
- getty: fix terminal scrollback issues after login on console TTY. Mouse
scroll wheel and Shift+PgUp/PgDn sometimes would not work properly
- Fix flickering and artifacts in `initctl top`
- Fix file descriptor leak in `initctl top`
- Fix incorrect "max" limit reporting in `initctl [top|cgroup|--json]` output
for child cgroups. Resolves effective limits by walking the cgroup hierarchy
- Fix issue with running Finit under `bootchartd` (bootchart2 project), always
set `PATH` and `SHELL` envs early, before attempting to mount `/proc` et al
- Always reset ownership and permissions on TTY device nodes before launching getty
- Ensure mount/unmount skips `noauto` entries
@@ -1885,7 +2003,9 @@ Major bug fix release.
* Initial release
[UNRELEASED]: https://github.com/finit-project/finit/compare/4.14...HEAD
[UNRELEASED]: https://github.com/finit-project/finit/compare/4.17...HEAD
[4.17]: https://github.com/finit-project/finit/compare/4.16...4.17
[4.16]: https://github.com/finit-project/finit/compare/4.15...4.16
[4.15]: https://github.com/finit-project/finit/compare/4.14...4.15
[4.14]: https://github.com/finit-project/finit/compare/4.13...4.14
[4.13]: https://github.com/troglobit/finit/compare/4.12...4.13
+37 -4
View File
@@ -14,11 +14,11 @@ specified separated by comma. Multiple conditions are logically AND'ed
during evaluation, i.e. all conditions must be satisfied in order for a
service to run.
A special syntax, using a leading `!` in run/task/service conditions,
denote if a:
Two special prefixes can be used inside the angle brackets:
- service does not support `SIGHUP`
- run/task should not block runlevel changes (i.e., bootstrap)
- `!` -- service does not support `SIGHUP` (noreload), or run/task
should not block runlevel changes (i.e., bootstrap)
- `~` -- propagate reload from this dependency, see below
Finit guarantees by default that all run/tasks run (at least) once
per runlevel. For most tasks this is a good default, for example
@@ -53,6 +53,30 @@ moving to the next runlevel after bootstrap, so we set `<!>`:
task [S0123456789] <!sys/pwr/fail> name:pwrfail initctl poweroff -- Power failure, shutting down
Propagating Reload in Dependencies
-----------------------------------
By default, when a service reloads during `initctl reload`, dependent
services are paused (`SIGSTOP`) and simply resumed (`SIGCONT`) when the
condition is reasserted. This is correct for barrier-style dependencies
like `<pid/syslogd>`, where dependents just need syslogd running and do
not care if it reloads its config.
For services that need to react when their upstream reloads, the `~`
prefix propagates the reload from the dependency:
service <pid/svc_a> name:svc_b /sbin/svc_b -- Needs A (barrier)
service <!~pid/svc_b> name:svc_c /sbin/svc_c -- Propagate reload from B
Here, `<~pid/svc_b>` means: propagate a reload of `svc_b` to `svc_c`.
When `svc_b` reloads, `svc_c` will be restarted (because of `!`,
noreload) instead of merely resumed. If `svc_c` supported `SIGHUP`
(no `!` prefix), it would be sent `SIGHUP` instead.
This is similar to systemd's `PropagatesReloadTo=` directive, but
declared on the consumer side rather than the provider side.
Triggering
----------
@@ -194,6 +218,10 @@ When Finit configuration files are changed and the `initctl reload`
command is called, it is expected of services to touch their PID files
for Finit to reassert their conditions.
Similarly, when a single service is reloaded with `initctl reload NAME`,
its conditions are cleared and reasserted, ensuring dependent services
are properly updated.
Daemons that don't create PID files, or fail to touch them on reload,
can be worked around by using the `pid:/path/to/file.pid` syntax in
the service stanza for the daemon. It is far from optimal since any
@@ -277,6 +305,11 @@ This STOP/CONT handling minimizes the number of unnecessary service
restarts that would otherwise occur because a depending service was sent
`SIGHUP` for example.
Services with the `~` prefix are an exception to this rule: when their
conditions return to `on` after being in `flux`, the reload is propagated
-- the service is reloaded (SIGHUP) or restarted (noreload `!`) instead
of simply being resumed.
Therefore, any plugin that supplies Finit with conditions must ensure
that their state is updated after each reconfiguration. This can be
done by binding to the `HOOK_SVC_RECONF` hook. For an example of how
+20 -10
View File
@@ -115,9 +115,10 @@ Common capabilities include (see `man 7 capabilities` for the complete list):
- Only grant what the service actually needs
- Don't grant `cap_sys_admin` unless absolutely necessary
2. **Always specify a user**
- Always use `@user` to drop to a non-root user
- Capabilities work best when combined with user separation
2. **Specify a user (preferably non-root)**
- The `@user` directive is **required** for `caps:` to take effect
- For ambient capabilities (`^`), use a non-root user (not `@root`)
- Example: `@www-data`, `@nginx`, `@tcpdump`
3. **Use ambient capabilities (`^`)**
- The `^` prefix ensures capabilities survive exec()
@@ -162,15 +163,24 @@ ps -o user,pid,cmd -p $(pidof nginx)
## Limitations
- Capabilities are only applied when both `@user` and `caps:` are specified
- The service must drop to a non-root user for capabilities to be effective
- The `caps:` directive requires `@user` to be specified for it to take effect
- Without `@user`, the service runs as root with full capabilities and
the `caps:` configuration is silently ignored
- You can use `@root` with `caps:`, but see below about ambient capabilities
- For ambient capabilities (`^`, recommended), the user **must be non-root**
- Using `@root` with `caps:^...` will not work effectively, as ambient
capabilities are only added to the effective set when euid ≠ 0
- Use inheritable (`%`) or bounding (`!`) capabilities with `@root` if needed
- Services without `caps:` use standard privilege dropping:
- Services with `@user` (non-root) have no special capabilities
- Services without `@user` run as root with full capabilities
- Some very old binaries may not work correctly with ambient capabilities
- File system capabilities are not managed by Finit (use `setcap` for that)
## See Also
- `man 7 capabilities` - Linux capabilities overview
- `man 3 cap_iab` - IAB capability API documentation
- `man 8 setcap` - Set file capabilities
- `man 8 getcap` - Query file capabilities
- `man 1 capsh` - Capability shell wrapper
- [capabilities(7)](https://man7.org/linux/man-pages/man7/capabilities.7.html) - Linux capabilities overview
- [cap_iab(3)](https://man7.org/linux/man-pages/man3/cap_iab.3.html) - IAB capability API documentation
- [setcap(8)](https://man7.org/linux/man-pages/man8/setcap.8.html) - Set file capabilities
- [getcap(8)](https://man7.org/linux/man-pages/man8/getcap.8.html) - Query file capabilities
- [capsh(1)](https://man7.org/linux/man-pages/man1/capsh.1.html) - Capability shell wrapper
+67 -21
View File
@@ -1,16 +1,29 @@
There are three major cgroup configuration directives:
Finit provides three different cgroup directives for controlling resource allocation:
1. Global top-level group: `init`, `system`, `user`, or a custom group
2. Selecting a top-level group for a set of run/task/services
3. Per run/task/service limits
1. **Top-level cgroup definition**: `cgroup NAME settings`
- Defines a top-level cgroup (e.g., `init`, `system`, `user`) with default settings
- Space-separated syntax
- Example: `cgroup system cpu.weight:9700`
2. **Global cgroup selector**: `cgroup.NAME[,options]` (standalone directive)
- Sets the default cgroup for subsequent services in a `.conf` file
- Dot-separated with optional comma-separated options
- Example: `cgroup.maint` or `cgroup.system,delegate`
3. **Per-service cgroup option**: `cgroup.NAME[,options]` or `cgroup:options`
- Overrides the cgroup for a specific service
- Part of the service directive line
- Example: `service [...] cgroup.maint,mem.max:1G /path/to/cmd`
> [!NOTE]
> Linux cgroups and details surrounding values are not explained in the
> Finit documentation. The Linux admin-guide cover this well:
> Finit documentation. The Linux admin-guide covers this well:
> <https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html>
Top-level Group Configuration
-----------------------------
Top-level Cgroup Definition
----------------------------
**Syntax:** `cgroup NAME settings`
# Top-level cgroups and their default settings. All groups mandatory
# but more can be added, max 8 groups in total currently. The cgroup
@@ -34,22 +47,37 @@ By default, the `system/` cgroup is selected for almost everything. The
`init/` cgroup is reserved for PID 1 itself and its closest relatives.
The `user/` cgroup is for local TTY logins spawned by getty.
Selecting Cgroups
------------------
Global Cgroup Selector
----------------------
To select a different top-level cgroup, e.g. `maint/`, one can either
define it for a group of run/task/service directives in a `.conf` or per
each stanza:
**Syntax:** `cgroup.NAME[,options]` (standalone directive)
To select a different top-level cgroup, e.g. `maint/`, for a group of
run/task/service directives in a `.conf` file, use the `cgroup.NAME`
directive as a standalone line:
cgroup.maint
service [...] <...> /path/to/foo args -- description
service [...] <...> /path/to/bar args -- description
or
Both services will run in the `maint/` cgroup.
You can also include options with the global selector:
cgroup.system,delegate
service [...] <...> /path/to/foo args -- description
Per-Service Cgroup Option
--------------------------
**Syntax:** `cgroup.NAME[,options]` or `cgroup:options` (service option)
To override the cgroup for a specific service, use the `cgroup.NAME`
option within the service directive:
service [...] <...> cgroup.maint /path/to/foo args -- description
The latter form also allows per-stanza limits. Two syntaxes are supported:
This form also allows per-service limits. Two syntaxes are supported:
**New comma-separated syntax (recommended):**
@@ -59,6 +87,11 @@ The latter form also allows per-stanza limits. Two syntaxes are supported:
service [...] <...> cgroup.maint:cpu.max:10000,mem.max:655360 /path/to/foo args -- description
You can also apply options to the current default cgroup (without changing it)
using the `cgroup:options` syntax:
service [...] <...> cgroup:cpu.max:10000,mem.max:655360 /path/to/foo args -- description
Both syntaxes work identically. The new comma-separated syntax is recommended
as it's more consistent with other option parsing in Finit.
@@ -88,11 +121,11 @@ The syntax supports combining the name override with other options:
Or with delegation:
service [2345] user:podman group:podman \
service [2345] @podman:podman \
cgroup.containers,name:podman,delegate,mem.max:4G \
/usr/bin/podman system service -- Podman API
A daemon using `SCHED_RR` currently need to run outside the default cgroups.
A daemon using `SCHED_RR` currently needs to run outside the default cgroups.
service [...] <...> cgroup.root /path/to/daemon arg -- Real-Time process
@@ -102,12 +135,12 @@ Cgroup Delegation
For services that need to create their own child cgroups (container runtimes
like Docker, Podman, systemd-nspawn, LXC), use the `delegate` option:
service [2345] user:dockerd group:dockerd \
service [2345] @dockerd:dockerd \
cgroup.system,delegate /usr/bin/dockerd -- Docker daemon
Or with the old colon syntax:
service [2345] user:dockerd group:dockerd \
service [2345] @dockerd:dockerd \
cgroup.system:delegate /usr/bin/dockerd -- Docker daemon
This allows the container runtime to:
@@ -126,23 +159,36 @@ When delegation is enabled, Finit:
**Requirements:**
- The service should specify `user:` and `group:` for proper ownership
- The service should specify `@user:group` for proper ownership
- Controllers are delegated from the parent cgroup
**Example with additional config (new syntax):**
service [2345] user:podman group:podman \
service [2345] @podman:podman \
cgroup.containers,delegate,mem.max:4G \
/usr/bin/podman system service -- Podman API
**Or with old syntax:**
service [2345] user:podman group:podman \
service [2345] @podman:podman \
cgroup.containers:delegate,mem.max:4G \
/usr/bin/podman system service -- Podman API
Both examples delegate the cgroup while also setting a 4GB memory limit.
**Container template example:**
Here's a real-world example from [Infix OS](https://github.com/kernelkit/infix)
for running rootful podman container instances using delegation:
sysv log:prio:local1,tag:%i kill:30 pid:!/run/container:%i.pid \
pre:0,/usr/sbin/container cleanup:0,/usr/sbin/container \
cgroup.system,delegate \
[2345] <!> :%i container -n %i -- container %i
This template uses `sysv` type with delegation, demonstrating that cgroup
delegation works with different service types, not just `service`.
**Cgroup structure with delegation:**
Initially, the service process runs directly in the cgroup root:
+5 -4
View File
@@ -127,11 +127,12 @@ Resource Limits
Set the hard or soft limit for a resource, or both if that argument is
omitted. `RESOURCE` is the lower-case `RLIMIT_` string constants from
`setrlimit(2)`, without prefix. E.g. to set `RLIMIT_CPU`, use `cpu`.
LIMIT is an integer that depends on the resource being modified, see
the man page, or the kernel `/proc/PID/limits` file, for details.
Finit versions before v3.1 used `infinity` for `unlimited`, which is
still supported, albeit deprecated.
[setrlimit(2)](https://man7.org/linux/man-pages/man2/setrlimit.2.html),
or the kernel `/proc/PID/limits` file, for details. Finit versions
before v3.1 used `infinity` for `unlimited`, which is still supported,
albeit deprecated.
# No process is allowed more than 8MB of address space
rlimit hard as 8388608
+7 -1
View File
@@ -43,9 +43,10 @@ started at any time by running `initctl start <service>`.
Other run/task/service options are:
* `caps:...` -- see the [Linux Capabilities](capabilities.md) section
* `cgroups:...` -- see the [Cgroups](cgroups.md) section
* `cgroup.NAME[,opts]` or `cgroup:opts` -- see the [Cgroups](cgroups.md) section
* `env:[-]/path/to/env` -- see the [Service Environment](service-env.md) section
* `log:...` -- see [Redirecting Output](logging.md#redirecting-output)
* `tty:<dev>` -- see [Controlling TTY](tty.md#controlling-tty)
* `nowarn` -- see [Conditional Loading](services.md#conditional-loading)
* `notify:...` -- see [Service Synchronization](service-sync.md)
* `if:...` -- see [Conditional Execution](services.md#conditional-execution)
@@ -69,6 +70,11 @@ confines of that the following options are available:
* `respawn` -- bypasses the `restart` mechanism completely, allows
endless restarts. Useful in many use-cases, but not what `service`
was originally designed for so not the default behavior
* `remain:yes` -- for `run` and `task` only. Prevents the task from
re-running on runlevel re-entry and ensures the `post:` script runs
when the task is explicitly stopped or leaves its valid runlevels.
Similar to systemd's `RemainAfterExit=yes`. See [Task and Run](task-and-run.md)
for more details
* `oncrash:reboot` -- when all retries have failed, and the service
has *crashed*, if this option is set the system is rebooted
* `oncrash:script` -- similarly, but instead of rebooting, call the
+24 -1
View File
@@ -47,6 +47,14 @@ waits for another service, `zebra`, to have created its PID file in
*all* files in `/var/run`, for each file named `*.pid`, or `*/pid`,
Finit opens it and find the matching `NAME:ID` using the PID.
The condition can be prefixed with `!` and/or `~`:
- `<!pid/zebra>` -- `ospfd` does not support `SIGHUP` (noreload)
- `<~pid/zebra>` -- propagate reload from `zebra` to `ospfd`
- `<!~pid/zebra>` -- both: noreload and propagate reload
For details, see the [Finit Conditions](../conditions.md) document.
Some services do not maintain a PID file and rather than patching each
application Finit provides a workaround. A `pid` keyword can be set
to have Finit automatically create (when starting) and later remove
@@ -116,10 +124,25 @@ Non-privileged Services
Every `run`, `task`, or `service` can also list the privileges the
`/path/to/cmd` should be executed with. Prefix the command with
`@USR[:GRP]`, group is optional, like this:
`@USR[:GRP[,SUPP,...]]`, where group and supplementary groups are
optional, like this:
run [2345] @joe:users logger "Hello world"
Finit reads the user's supplementary group membership from `/etc/group`
automatically. Any groups the user belongs to will be inherited by
the service.
To specify additional supplementary groups beyond those in `/etc/group`,
append them after the primary group, separated by commas:
service @caddy:caddy,ssl-cert /usr/bin/caddy run
This runs the `caddy` service as user `caddy`, with primary group
`caddy`, inheriting any groups `caddy` is a member of in `/etc/group`,
plus the additional `ssl-cert` group. This is useful when a service
needs access to resources owned by groups not listed in `/etc/group`.
For multiple instances of the same command, e.g. a DHCP client or
multiple web servers, add `:ID` somewhere between the `run`, `task`,
`service` keyword and the command, like this:
+41
View File
@@ -44,3 +44,44 @@ redirects can be used:
Please note, `;`, `&&`, `||`, and similar are *not supported*. Any
non-trivial constructs are better placed in a separate shell script.
remain:yes
----------
By default, a `run` or `task` will re-run each time its runlevel is
entered, and its `post:` script does not run on completion.
With `remain:yes`, the task runs once and does not re-run on runlevel
re-entry:
task [2345] remain:yes /usr/sbin/setup-firewall -- Firewall setup
This has the following effects:
* The task does not re-run on runlevel re-entry
* The `post:` script runs when:
- The task is explicitly stopped (`initctl stop NAME`)
- The task leaves its valid runlevels (e.g., runlevel change)
This is useful for tasks that set up persistent state where:
* Cleanup should only happen on explicit stop or when leaving valid runlevels
* The setup should not be re-run on every runlevel entry
**Example:** Setting up firewall rules with cleanup on shutdown:
```
task [2345] remain:yes \
post:/usr/sbin/teardown-firewall \
/usr/sbin/setup-firewall -- Firewall setup
```
The firewall rules are created once. The `post:` script runs when
entering runlevel 0 (halt) or 6 (reboot), or on explicit stop.
> [!NOTE]
> The `remain:yes` option is not supported for bootstrap-only tasks
> (tasks with only runlevel S). Bootstrap tasks are deleted immediately
> after completion, and their `post:` scripts never run. A warning is
> logged if `remain:yes` is used on such tasks.
+28 -1
View File
@@ -83,4 +83,31 @@ board bringup and system debugging it can come in handy.
One can also use the `service` stanza to start a stand-alone shell:
service [12345] /bin/sh -l
service [12345] /bin/sh -l
Controlling TTY for Services
----------------------------
The `tty:<dev>` option gives a `run`, `task`, or `service` a controlling
terminal on the given device. The device is opened, set as the
controlling terminal for the session (after `setsid()`), and connected to
the process's stdin, stdout, and stderr. A default `TERM` environment
variable is set based on the device type: `vt102` for serial lines and
`linux` for virtual terminals.
`<dev>` may be a device node like `/dev/ttyS0`, or the special keyword
`@console` (see above). Note that `@console` expands only to the
first console, not all.
When `tty:` is combined with `log:`, stdout and stderr are redirected
to the log sink instead of the TTY, but stdin remains connected to the
TTY device.
> The `tty:<dev>` option is for `run`, `task`, and `service` stanzas only.
> The `tty` directive itself (for getty/login) has its own syntax, see
> above.
**Example:**
service [2345] tty:/dev/ttyS0 /usr/sbin/foo -- Foo on serial console
task [S] tty:@console my-setup-script -- Board bringup on console
+105 -46
View File
@@ -1,80 +1,139 @@
Distro Recommendations
======================
By default Finit uses the following directories for configuration files:
Finit supports two directory layouts for managing service configurations:
1. **Simple layout** - all configuration files directly in `/etc/finit.d/`
2. **Distro layout** - separate `available/` and `enabled/` directories
The distro layout is recommended for distributions that need clear
separation between installed and enabled services, and want to use the
`initctl enable/disable` commands for service management.
## Simple Layout
Finit supports using a flat directory structure:
```
/etc/
|-- finit.d/ -- Regular (enabled) services
|-- finit.d/ -- All enabled services
| |-- lighttpd.conf
| `- *.conf
|-- finit.conf -- Bootstrap tasks and services
:
```
To enable a service one simply drops a small configuration file in the
`/etc/finit.d/` directory. This practice works with systems that
keep disabled services elsewhere, or generates them as needed from some
other tool.
In this layout, you enable a service by placing its `.conf` file in
`/etc/finit.d/` and disable it by removing the file. This works well for
embedded systems or custom setups where services are managed by other tools
or generated dynamically.
Distributions, however, may want a clearer separation of enabled and
available (installed but not enabled) services. They may even want to
customize the directories used, for brand labeling or uniformity.
> [!NOTE] No tooling support
> The `initctl enable/disable` commands **do not work** with this layout.
> You must manually manage files in `/etc/finit.d/`.
To that end Finit allows for a sub-directory `/etc/finit.d/available/`
where installed but disabled services can reside. Adding a symlink to a
configuration in this sub-directory enables the service, but it will not
be started.
## Distro Layout
To change the default configuration directory and configuration file
names the Finit `configure` script offers the following two options at
build time:
Distributions typically want clearer separation between available (installed)
and enabled services. Finit supports this through the `available/` and
`enabled/` subdirectories:
```
/etc/
|-- finit.d/
| |-- available/ -- Installed but disabled services
| | |-- httpd.conf
| | |-- ntpd.conf
| | `-- sshd.conf
| |-- enabled/ -- Enabled services (symlinks)
| | `- sshd.conf -- Symlink to ../available/sshd.conf
|-- finit.conf -- Bootstrap tasks and services
:
```
In this layout:
- **available/** contains all installed service configurations
- **enabled/** contains symlinks to configurations that should start at boot
- Services are enabled/disabled by creating/removing symlinks
> [!IMPORTANT] Recommended
> This is the recommended layout. In fact, the `initctl enable/disable`
> commands **require** this layout with both `available/` and `enabled/`
> directories and will not work without it.
## Managing Services with initctl
When using the distro layout, the `initctl` tool provides convenient commands
for managing service configurations:
```
list List all .conf in /etc/finit.d/
enable <CONF> Enable .conf by creating symlink in enabled/
disable <CONF> Disable .conf by removing symlink from enabled/
reload Reload *.conf in /etc/finit.d/ (activate changes)
```
Example usage:
```bash
# Enable sshd (creates enabled/sshd.conf -> ../available/sshd.conf)
initctl enable sshd
# Disable httpd (removes enabled/httpd.conf symlink)
initctl disable httpd
# Apply changes (reload configuration and start/stop services)
initctl reload
```
The `.conf` suffix is optional - `initctl` adds it automatically if missing.
> [!NOTE] Remember `initctl reload`
> Changes made with `enable`/`disable` take effect only after running
> `initctl reload`, unless Finit was built with `--enable-auto-reload`
> which automatically detects and applies configuration changes.
### Service Overrides
The `initctl` tool only operates on symlinks in the `enabled/` directory.
If you place a regular (non-symlink) `.conf` file directly in the parent
directory (e.g., `/etc/finit.d/`), `initctl` will ignore it. This allows
you to create system-level overrides that cannot be accidentally disabled
by package management tools.
## Customizing Directory Paths
Distributions can customize the directory names and locations at build time
using the Finit `configure` script:
```sh
./configure --with-rcsd=/etc/init.d --with-config=/etc/init.d/init.conf
```
> [!IMPORTANT]
> Remember `--prefix` et al as well, the default is likely *not* what
> you want. See the [build docs][1] for details.
This changes the default `/etc/finit.d/` to `/etc/init.d/` and moves the
bootstrap configuration file accordingly.
The resulting directory structure is depicted below. Please notice how
`/etc/finit.conf` now resides in the same sub-directory as a non-symlink
`/etc/init.d/init.conf`:
> [!IMPORTANT] Remember to set `--prefix`
> Remember to set `--prefix` and related options appropriately. The default
> prefix is `/usr/local`, which is likely not what you want for a system
> init. See the [build documentation][1] for details.
Example with custom paths:
```
/etc/
|-- init.d/
| |-- available/ -- Regular (disabled) services
| |-- available/ -- Installed services
| | |-- httpd.conf
| | |-- ntpd.conf
| | `-- sshd.conf
| |-- sshd.conf -- Symlink to available/sshd.conf
| |-- enabled/ -- Enabled services (symlinks)
| | `- sshd.conf -- Symlink to ../available/sshd.conf
| `- init.conf -- Bootstrap tasks and services
:
```
To facilitate the task of managing configurations, be it a service,
task, run, or other stanza, the `initctl` tool has a a few built-in
commands:
```
list List all .conf in /etc/finit.d/
enable <CONF> Enable .conf in /etc/finit.d/available/
disable <CONF> Disable .conf in /etc/finit.d/[enabled/]
reload Reload *.conf in /etc/finit.d/ (activates changes)
```
To enable a service like `sshd.conf`, above, use
initctl enable sshd
The `.conf` suffix is not needed, `initctl` adds it implicitly if it is
missing. The `disable` command works in a similar fashion.
Note, however, that `initctl` only operates on symlinks and it always
requires the `available/` sub-directory. Any non-symlink in the parent
directory, here `/etc/init.d/`, will be considered a system override by
`initctl`.
Notice how both the service directory and bootstrap configuration file now
use the custom `/etc/init.d/` path specified at build time.
[1]: build.md#configure
+104
View File
@@ -1,6 +1,10 @@
Finit Features
==============
This page highlights some of Finit's key features with examples and usage
scenarios. For complete documentation, configuration syntax, and advanced
options, see the [Configuration](config/index.md) section.
**Process Supervision**
Start, monitor and restart services should they fail.
@@ -162,6 +166,28 @@ See the [Linux Capabilities](config/capabilities.md) section for detailed
information, examples, and security best practices.
**Supplementary Groups**
Finit supports supplementary groups for services, allowing them to access
resources owned by multiple groups without running as root. This complements
capabilities for fine-grained privilege control.
```conf
service @caddy:caddy,ssl-cert /usr/bin/caddy run
```
In this example, the Caddy web server runs as user `caddy` with primary group
`caddy`, but also has access to resources owned by the `ssl-cert` group (such
as TLS certificates).
Finit automatically reads the user's supplementary group membership from
`/etc/group`. Additional groups can be specified explicitly using the syntax
`@user:group,sup1,sup2,...`.
See the [Non-privileged Services](config/services.md#non-privileged-services)
section for more information.
**Cgroups**
Finit supports cgroups v2 and comes with the following default groups in
@@ -207,4 +233,82 @@ commands for details.
> disabled early at boot.
**Service Management**
Finit includes the `initctl` tool for managing services and system state at
runtime. Key capabilities include:
- **Enable/Disable services**: Manage which services start at boot by moving
configuration files between `/etc/finit.d/available` and
`/etc/finit.d/enabled`
- **Start/Stop/Restart**: Control individual services without requiring a
full system reboot
- **Status monitoring**: View service state, PID, uptime, and resource usage
- **Condition management**: Set and clear user-defined conditions to control
service dependencies
- **Cgroup monitoring**: Real-time process and resource monitoring with
`initctl top`, similar to the traditional `top` command but cgroup-aware
Example commands:
```bash
initctl enable myservice # Enable service for next boot
initctl start myservice # Start service now
initctl status # Show all services
initctl top # Interactive resource monitor
initctl cond set usr/custom # Set custom condition
```
See the [Commands & Status](initctl.md) section for complete documentation.
**Rescue Mode**
Finit provides a built-in rescue mode for system recovery and maintenance.
When booting with the `rescue` kernel parameter, the system enters a
protected maintenance shell.
If the bundled `sulogin` program is available (from Finit, util-linux, or
BusyBox), you'll be prompted for the root password before accessing the
maintenance shell. This provides secure access for system recovery.
If `sulogin` is not available, Finit falls back to reading
`/lib/finit/rescue.conf` and boots the system in a limited maintenance mode.
```
# Kernel command line
linux /vmlinuz root=/dev/sda1 rescue
```
In rescue mode, `initctl` will not work. After fixing the problem, use
`reboot -f` to force reboot.
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
+20 -14
View File
@@ -1,7 +1,7 @@
Introduction
============
![Alpine screenshot](img/alpine-screenshot2.png){ align=right }
![Alpine screenshot](img/alpine-screenshot2.png){ align=right width=40% }
> Reverse engineered from the [EeePC fastinit][]
> "gaps filled with frog DNA …"
@@ -17,26 +17,31 @@ PID file monitoring, or [conditions](conditions.md).
Features
--------
* [Runlevels][5], defined per service
* One-shot tasks, services (daemons), or [SysV init][4] start/stop scripts
* Runparts and `/etc/rc.local` support
* [Runlevels](config/runlevels.md), defined per service
* [One-shot tasks](config/task-and-run.md), [services](config/services.md) (daemons), or [SysV init][4] start/stop scripts
* [Runparts](config/runparts.md) and `/etc/rc.local` support
* Process supervision similar to [systemd][]
* Sourcing environment files
* Conditions for network/process/custom dependencies
* Readiness notification; PID files (native) for synchronizing system
* Fine-grained privilege control:
- [Linux capabilities](config/capabilities.md) for minimal required privileges
- [Supplementary groups](config/services.md#non-privileged-services) for multi-group resource access
* [Sourcing environment files](config/service-env.md)
* [Conditions](conditions.md) for network/process/custom dependencies
* [Readiness notification](config/service-sync.md); PID files (native) for synchronizing system
startup, support for systemd [sd_notify()][], or [s6 style][] too
* Limited support for [tmpfiles.d(5)][] (no aging, attributes, or subvolumes)
* Pre/Post script actions
* [Pre/Post script actions](config/service-wrappers.md)
* Rudimentary [templating support](config/templating.md)
* Tooling to enable/disable services
* Built-in getty
* [Tooling](initctl.md) to enable/disable services
* [Automatic reload](features.md#automatic-reload) of modified configuration files (optional)
* [Built-in getty](config/tty.md)
* Built-in watchdog, with support for hand-over to [watchdogd][]
* Built-in support for Debian/BusyBox [`/etc/network/interfaces`][3]
* Cgroups v2, both configuration and monitoring in `initctl top`
* Plugin support for customization
* Proper rescue mode with bundled `sulogin` for protected maintenance shell
* [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 to kernel ring buffer before `syslogd` has started, see the
* [Logging](config/logging.md) to kernel ring buffer before `syslogd` has started, see the
recommended [sysklogd][] project for complete logging integration
and how to log to the kernel ring buffer from scripts using `logger`
@@ -64,6 +69,7 @@ and proposed extensions.
[4]: https://en.wikipedia.org/wiki/Init
[5]: https://en.wikipedia.org/wiki/Runlevel
[6]: https://github.com/finit-project/finit
[dinit]: https://davmac.org/projects/dinit/
[systemd]: https://www.freedesktop.org/wiki/Software/systemd/
[sd_notify()]: https://www.freedesktop.org/software/systemd/man/sd_notify.html
[s6 style]: https://skarnet.org/software/s6/notifywhenup.html
+22
View File
@@ -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.
+10
View File
@@ -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
+149
View File
@@ -0,0 +1,149 @@
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
# The tty:@console stanza is required so cryptsetup can prompt for a passphrase
run [S] name:cryptsetup tty:@console /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)
+45
View File
@@ -398,6 +398,20 @@ mechanism completely, allows endless restarts. Useful in many
use-cases, but not what
.Cm service
was originally designed for so not the default behavior.
.It Cm remain:yes
for
.Cm run
and
.Cm task
only. Prevents the task from re-running on runlevel re-entry and
ensures the
.Cm post:
script runs when the task is explicitly stopped or leaves its valid
runlevels. Similar to systemd's
.Cm RemainAfterExit=yes .
.Pp
.Sy Note:
not supported for bootstrap-only tasks (runlevels [S] only).
.It Cm oncrash:reboot
when all retries have failed, and the service
has
@@ -730,6 +744,37 @@ is
and the default
.Cm tag
identity is the basename of the service or run/task command.
.It Cm tty:\&<dev>
Give the
.Cm run , task ,
or
.Cm service
a controlling terminal on
.Cm <dev> .
The device is opened, set as the controlling terminal for the session
(after
.Fn setsid ) ,
and connected to stdin, stdout, and stderr.
A default
.Ev TERM
variable is set based on the device type:
.Cm vt102
for serial lines and
.Cm linux
for virtual terminals.
.Cm <dev>
may be a device node like
.Pa /dev/ttyS0 ,
or the special keyword
.Cm @console
(see TTYs and Consoles above).
When combined with
.Cm log: ,
stdout and stderr are redirected to the log sink instead, but stdin
remains connected to the TTY device.
.Bd -unfilled -offset indent
service [2345] tty:/dev/ttyS0 /usr/sbin/foo -- Foo on serial console
.Ed
.El
.Sh RESCUE MODE
Finit supports a rescue mode which is activated by the
+29 -21
View File
@@ -1,6 +1,6 @@
site_name: Documentation
site_name: ""
site_description: Fast Init for Linux Systems
site_url: http://finit-project.github.io/finit/
site_url: http://finit-project.github.io
repo_url: https://github.com/finit-project/finit
repo_name: Finit Project
copyright: Copyright &copy; 2008-2025 Joachim Wiberg
@@ -11,6 +11,7 @@ extra_css:
exclude_docs: |
TODO.md
ChangeLog.md
theme:
logo: img/logo.png
@@ -22,7 +23,7 @@ theme:
- navigation.instant
- navigation.instant.progress
- navigation.tracking
# - navigation.tabs
- navigation.tabs
# - navigation.sections
# - navigation.expand
# - navigation.top
@@ -49,20 +50,16 @@ theme:
name: Switch to light mode
nav:
- Introduction: index.md
- Example: example.md
- Features: features.md
- Commands & Status: initctl.md
- Rebooting & Halting: commands.md
- Runlevels: runlevels.md
- Runparts & /etc/rc.local: runparts.md
- Getting Started:
- Introduction: index.md
- Features: features.md
- Quick Example: example.md
- Building: build.md
- Configuration:
- Overview: config/index.md
- Files & Layout: config/files.md
- Conditions: conditions.md
- Cgroups: config/cgroups.md
- Capabilities: config/capabilities.md
- Environment Variables: config/env.md
- Runlevels: config/runlevels.md
- One-shot Tasks: config/task-and-run.md
- Services:
- Syntax: config/services.md
@@ -71,24 +68,30 @@ nav:
- Synchronization: config/service-sync.md
- Wrapper Scripts: config/service-wrappers.md
- Run-parts Scripts: config/runparts.md
- Runlevels: config/runlevels.md
- TTY & Console: config/tty.md
- Templating: config/templating.md
- Environment Variables: config/env.md
- Logging: config/logging.md
- Cgroups: config/cgroups.md
- Capabilities: config/capabilities.md
- Templating: config/templating.md
- SysV Compatibility: config/sysv.md
- Rescue Mode: config/rescue.md
- Limitations: config/limitations.md
- Usage:
- Building: build.md
- Command Line: cmdline.md
- Plugins: plugins.md
- Services: service.md
- Commands & Status: initctl.md
- Switch Root: switchroot.md
- Rebooting & Halting: commands.md
- Command Line Options: cmdline.md
- Managing Services: service.md
- Signals: signals.md
- Reference:
- Distributions: distro.md
- keventd: keventd.md
- Runlevels Overview: runlevels.md
- Runparts & rc.local: runparts.md
- Plugins: plugins.md
- Watchdog: watchdog.md
- keventd: keventd.md
- Service State Machine: state-machine.md
- Distributions: distro.md
- Requirements: requirements.md
markdown_extensions:
@@ -109,6 +112,11 @@ markdown_extensions:
plugins:
- search
- callouts
- glightbox
extra:
homepage: https://finit-project.github.io/
social:
- icon: fontawesome/brands/github
link: https://github.com/finit-project/finit
name: Finit on GitHub
+8
View File
@@ -29,6 +29,10 @@ if BUILD_NETLINK_PLUGIN
libplug_la_SOURCES += netlink.c
endif
if BUILD_PLYMOUTH_PLUGIN
libplug_la_SOURCES += plymouth.c
endif
if BUILD_RESOLVCONF_PLUGIN
libplug_la_SOURCES += resolvconf.c
endif
@@ -76,6 +80,10 @@ if BUILD_NETLINK_PLUGIN
pkglib_LTLIBRARIES += netlink.la
endif
if BUILD_PLYMOUTH_PLUGIN
pkglib_LTLIBRARIES += plymouth.la
endif
if BUILD_RESOLVCONF_PLUGIN
pkglib_LTLIBRARIES += resolvconf.la
endif
+3 -3
View File
@@ -38,7 +38,7 @@
static void save(void *arg)
{
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "alsa-utils");
return;
}
@@ -51,7 +51,7 @@ static void save(void *arg)
static void restore(void *arg)
{
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "alsa-utils");
return;
}
@@ -62,7 +62,7 @@ static void restore(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "alsa-utils",
.hook[HOOK_BASEFS_UP] = { .cb = restore },
.hook[HOOK_SHUTDOWN] = { .cb = save }
};
+1 -1
View File
@@ -172,7 +172,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "bootmisc",
.hook[HOOK_MOUNT_POST] = { .cb = clean },
.hook[HOOK_BASEFS_UP] = { .cb = setup },
.depends = { "pidfile" },
+2 -2
View File
@@ -106,7 +106,7 @@ static void setup(void *arg)
char *cmd;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "dbus");
return;
}
@@ -164,7 +164,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "dbus",
.hook[HOOK_SVC_PLUGIN] = { .cb = setup },
};
+1 -1
View File
@@ -79,7 +79,7 @@ static void hscript_shutdown(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "hook-scripts",
.hook[HOOK_BANNER] = { .cb = hscript_banner },
.hook[HOOK_ROOTFS_UP] = { .cb = hscript_rootfs_up },
.hook[HOOK_MOUNT_ERROR] = { .cb = hscript_mount_error },
+1 -1
View File
@@ -228,7 +228,7 @@ static void coldplug(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "modprobe",
.hook[HOOK_BASEFS_UP] = { .cb = coldplug },
.depends = { "bootmisc", }
};
+1 -1
View File
@@ -213,7 +213,7 @@ static void load(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "modules-load",
.hook[HOOK_SVC_PLUGIN] = { .cb = load },
};
+22 -2
View File
@@ -406,9 +406,29 @@ static void nl_reconf(void *arg)
cond_reassert("net/");
}
/*
* Initial enumeration of existing interfaces and routes. Called from
* HOOK_SVC_PLUGIN which runs after the condition system is initialized.
* Interfaces that exist before finit starts (e.g., virtio-net in QEMU)
* won't generate RTM_NEWLINK events, so we must query for them.
*/
static void nl_enumerate(void *arg)
{
int sd;
sd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
if (sd < 0)
return;
nl_resync_ifaces(sd, 0);
nl_resync_routes(sd, 1);
close(sd);
}
static plugin_t plugin = {
.name = __FILE__,
.hook[HOOK_SVC_RECONF] = { .cb = nl_reconf },
.name = "netlink",
.hook[HOOK_SVC_RECONF] = { .cb = nl_reconf },
.hook[HOOK_SVC_PLUGIN] = { .cb = nl_enumerate },
.io = {
.cb = nl_callback,
.flags = PLUGIN_IO_READ,
+3 -2
View File
@@ -59,7 +59,7 @@ static int pidfile_add_path(struct iwatch *iw, char *path)
}
}
return iwatch_add(iw, path, IN_ONLYDIR | IN_CLOSE_WRITE);
return iwatch_add(iw, path, IN_ONLYDIR | IN_CLOSE_WRITE | IN_ATTRIB);
}
static void pidfile_update_conds(char *dir, char *name, uint32_t mask)
@@ -276,6 +276,7 @@ static void pidfile_reconf(void *arg)
if (cond_get(cond) == COND_ON)
continue;
dbg("Reassert condition %s", cond);
cond_set_path(cond_path(cond), COND_ON);
}
@@ -334,7 +335,7 @@ static void pidfile_init(void *arg)
* SIGSTP:ed (in state PAUSED) waiting for <net/iface/lo>.
*/
static plugin_t plugin = {
.name = __FILE__,
.name = "pidfile",
.hook[HOOK_BASEFS_UP] = { .cb = pidfile_init },
.hook[HOOK_SVC_RECONF] = { .cb = pidfile_reconf },
.depends = { "netlink" }, /* bootmisc depends on us */
+293
View File
@@ -0,0 +1,293 @@
/* Plymouth boot splash plugin for Finit
*
* Copyright (c) 2012-2026 Aaron Andersen <aaron@fosslib.net>
*
* 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.
*/
/*
* This plugin integrates the Plymouth boot splash screen with Finit.
* It manages the plymouthd lifecycle across the full boot process:
*
* - HOOK_BANNER: Start plymouthd early, before console output.
* In initramfs, starts fresh. In stage 2, reuses
* the daemon carried over from initramfs if alive.
* - HOOK_ROOTFS_UP .. HOOK_SYSTEM_UP: Display status messages.
* - HOOK_SVC_UP: Tear down plymouth once boot is complete.
* - HOOK_SWITCH_ROOT: Notify plymouth of root filesystem change so
* the daemon survives the initramfs -> rootfs
* transition.
* - HOOK_SHUTDOWN: Restart plymouthd in shutdown mode for a
* splash during poweroff/reboot.
*
* The plugin is only activated when "splash" is present on the kernel
* command line. Plymouth requires devpts for VT takeover; fs_init()
* mounts it early enough for HOOK_BANNER.
*
* NOTE: The initramfs must include /etc/initrd-release. Plymouth
* checks for this file and, when present, prefixes its argv[0] with
* '@' so that the process is not killed during switch_root. Without
* it, plymouthd will not survive the initramfs-to-rootfs transition.
*/
#include "config.h"
#include "finit.h"
#include "helpers.h"
#include "conf.h"
#include "pid.h"
#include "plugin.h"
#include "sig.h"
#include "util.h"
#include "log.h"
#ifndef PLYMOUTH_PATH
#define PLYMOUTH_PATH "/sbin/plymouth"
#endif
#ifndef PLYMOUTHD_PATH
#define PLYMOUTHD_PATH "/sbin/plymouthd"
#endif
#define PLYMOUTH_PIDFILE "/run/plymouthd.pid"
static pid_t daemon_pid;
static int switching_root;
static int in_initramfs;
static int plymouth_cmd(const char *action)
{
char cmd[256];
snprintf(cmd, sizeof(cmd), PLYMOUTH_PATH " %s", action);
return run(cmd, NULL);
}
static void plymouth_message(const char *msg)
{
pid_t pid;
pid = fork();
if (pid == 0) {
sig_unblock();
execl(PLYMOUTH_PATH, PLYMOUTH_PATH,
"display-message", "--text", msg, NULL);
_exit(EX_OSERR);
}
}
static int plymouth_alive(void)
{
return daemon_pid > 0 && pid_alive(daemon_pid);
}
/* Start plymouthd in the given mode ("boot" or "shutdown"). */
static void plymouth_start(const char *mode)
{
char cmd[256];
int rc;
if (plymouth_alive())
return;
snprintf(cmd, sizeof(cmd),
PLYMOUTHD_PATH " --attach-to-session --mode %s --pid-file %s",
mode, PLYMOUTH_PIDFILE);
rc = run(cmd, NULL);
if (rc) {
warnx("plymouthd failed to start (exit %d)", rc);
return;
}
daemon_pid = pid_file_read(PLYMOUTH_PIDFILE);
if (daemon_pid <= 0) {
warnx("plymouthd started but no PID in %s", PLYMOUTH_PIDFILE);
return;
}
rc = plymouth_cmd("show-splash");
if (rc)
warnx("plymouth show-splash failed (exit %d)", rc);
}
static void plymouth_stop(void)
{
if (!plymouth_alive())
return;
plymouth_cmd("quit");
/*
* Don't poll -- we're in a finit hook, so finit's event loop
* is blocked and can't reap children. Trust that plymouthd
* exits after receiving the quit command.
*/
daemon_pid = 0;
unlink(PLYMOUTH_PIDFILE);
}
/*
* HOOK_BANNER - earliest possible hook, before any console output.
*
* In initramfs: start plymouthd fresh.
* In stage 2: reuse plymouthd from initramfs if still alive,
* otherwise start a new instance.
*/
static void plymouth_boot(void *arg)
{
in_initramfs = fexist("/etc/initrd-release");
if (rescue)
return;
enable_progress(0);
if (!in_initramfs) {
if (plymouth_cmd("--ping") == 0) {
daemon_pid = pid_file_read(PLYMOUTH_PIDFILE);
if (daemon_pid <= 0)
daemon_pid = 1; /* alive but unknown pid */
return;
}
}
plymouth_start("boot");
}
/*
* HOOK_SVC_UP - all services launched.
*
* In initramfs: keep splash alive for switch_root.
* In stage 2: boot is done, tear down plymouth.
*/
static void plymouth_boot_done(void *arg)
{
if (in_initramfs)
return;
plymouth_stop();
enable_progress(1);
}
/* HOOK_SWITCH_ROOT - initramfs transitioning to real root. */
static void plymouth_switchroot(void *arg)
{
switching_root = 1;
plymouth_message("Switching to root filesystem...");
if (plymouth_alive())
run(PLYMOUTH_PATH " update-root-fs --new-root-dir=/sysroot", NULL);
enable_progress(1);
}
/* HOOK_SHUTDOWN - entering runlevel 0 or 6. */
static void plymouth_shutdown(void *arg)
{
if (rescue || switching_root)
return;
enable_progress(0);
plymouth_start("shutdown");
}
static void on_rootfs_up(void *arg)
{
plymouth_message("Root filesystem mounted");
}
static void on_mount_post(void *arg)
{
plymouth_message("Mounting filesystems...");
}
static void on_basefs_up(void *arg)
{
plymouth_message("All filesystems mounted");
}
static void on_network_up(void *arg)
{
plymouth_message("Network is up");
}
static void on_system_up(void *arg)
{
plymouth_message("System ready");
}
static plugin_t plugin = {
.name = "plymouth",
.hook[HOOK_BANNER] = { .cb = plymouth_boot },
.hook[HOOK_ROOTFS_UP] = { .cb = on_rootfs_up },
.hook[HOOK_MOUNT_POST] = { .cb = on_mount_post },
.hook[HOOK_BASEFS_UP] = { .cb = on_basefs_up },
.hook[HOOK_NETWORK_UP] = { .cb = on_network_up },
.hook[HOOK_SYSTEM_UP] = { .cb = on_system_up },
.hook[HOOK_SVC_UP] = { .cb = plymouth_boot_done },
.hook[HOOK_SWITCH_ROOT] = { .cb = plymouth_switchroot },
.hook[HOOK_SHUTDOWN] = { .cb = plymouth_shutdown },
};
/*
* Check kernel command line for "splash" argument. Plymouth should
* only be activated when the user explicitly requests it.
*/
static int has_splash_arg(void)
{
char line[LINE_SIZE], *tok, *saveptr;
FILE *fp;
fp = fopen("/proc/cmdline", "r");
if (!fp)
return 0;
if (!fgets(line, sizeof(line), fp)) {
fclose(fp);
return 0;
}
fclose(fp);
for (tok = strtok_r(line, " \t\n", &saveptr); tok;
tok = strtok_r(NULL, " \t\n", &saveptr)) {
if (!strcmp(tok, "splash"))
return 1;
}
return 0;
}
PLUGIN_INIT(__init)
{
if (!has_splash_arg())
return;
plugin_register(&plugin);
}
PLUGIN_EXIT(__exit)
{
plugin_unregister(&plugin);
}
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+2 -2
View File
@@ -43,7 +43,7 @@ static void setup(void *arg)
glob_t gl;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "procps");
return;
}
@@ -69,7 +69,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "procps",
.hook[HOOK_BASEFS_UP] = {
.cb = setup
},
+1 -1
View File
@@ -51,7 +51,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "resolvconf",
.hook[HOOK_BASEFS_UP] = {
.cb = setup
},
+3 -3
View File
@@ -236,7 +236,7 @@ static void rtc_save(void *arg)
int fd, rc = 0;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "rtc");
return;
}
@@ -266,7 +266,7 @@ static void rtc_restore(void *arg)
int fd, rc = 0;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "rtc");
return;
}
@@ -321,7 +321,7 @@ static void update(uev_t *w, void *arg, int events)
static plugin_t plugin = {
.name = __FILE__,
.name = "rtc",
.hook[HOOK_BASEFS_UP] = {
.cb = rtc_restore
},
+1 -1
View File
@@ -180,7 +180,7 @@ static void sys_init(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "sys",
.hook[HOOK_BASEFS_UP] = { .cb = sys_init },
.depends = { "bootmisc", },
};
+1
View File
@@ -43,6 +43,7 @@
static void tty_watcher(void *arg, int fd, int events);
static plugin_t plugin = {
.name = "tty",
.io = {
.cb = tty_watcher,
.flags = PLUGIN_IO_READ,
+3 -3
View File
@@ -87,7 +87,7 @@ static void setup(void *arg)
int fd, err;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "urandom");
return;
}
@@ -188,7 +188,7 @@ static void save(void *arg)
mode_t prev;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "urandom");
return;
}
@@ -202,7 +202,7 @@ static void save(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "urandom",
.hook[HOOK_BASEFS_UP] = { .cb = setup },
.hook[HOOK_SHUTDOWN] = { .cb = save },
.depends = { "bootmisc", }
+1 -1
View File
@@ -104,7 +104,7 @@ static void usr_init(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "usr",
.hook[HOOK_BASEFS_UP] = { .cb = usr_init },
.depends = { "bootmisc", },
};
+2 -2
View File
@@ -39,7 +39,7 @@
static void setup(void *arg)
{
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "x11-common");
return;
}
@@ -48,7 +48,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "x11-common",
.hook[HOOK_SVC_PLUGIN] = { .cb = setup },
};
+1
View File
@@ -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 \
+74
View File
@@ -112,6 +112,8 @@ static int restart(svc_t *svc, void *user_data)
static int reload(svc_t *svc, void *user_data)
{
char cond[MAX_COND_LEN];
(void)user_data;
if (!svc)
@@ -122,6 +124,25 @@ static int reload(svc_t *svc, void *user_data)
else
service_timeout_cancel(svc);
/*
* Clear conditions before reload to ensure dependent services
* are properly updated. Only needed when the service does NOT
* support SIGHUP (noreload), because then it will be stopped
* and restarted, so conditions genuinely go away. When the
* service handles SIGHUP, its PID and pidfile persist, so the
* condition stays valid and dependents should not be disrupted.
*
* Note: only clear 'ready' for services where the pidfile
* inotify handler reasserts it (pid/none). For s6/systemd
* services readiness relies on their respective notification
* mechanism which may not re-trigger on SIGHUP.
*/
if (svc_is_noreload(svc)) {
cond_clear(mkcond(svc, cond, sizeof(cond)));
if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE)
service_ready(svc, 0);
}
svc_mark_dirty(svc);
service_step(svc);
@@ -245,6 +266,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;
@@ -403,6 +464,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 +573,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;
+36 -8
View File
@@ -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"
@@ -188,7 +190,8 @@ int cgroup_move_pid(const char *group, const char *name, int pid, int delegate)
}
if (rc) {
warn("Failed moving pid %d to cgroup %s", pid, path);
if (errno != ESRCH)
warn("Failed moving pid %d to cgroup %s", pid, path);
return -1;
}
}
@@ -377,7 +380,8 @@ static int cgroup_leaf_init(const char *group, const char *name, int pid, const
if (!strcmp(group, "root"))
return fnwrite(str("%d", pid), FINIT_CGPATH "/cgroup.procs");
if (cgroup_move_pid(group, name, pid, delegate))
/* Error if error is not "No such process" */
if (cgroup_move_pid(group, name, pid, delegate) && errno != ESRCH)
err(1, "Failed moving pid %d to group %s/%s", pid, group, name);
/* Set up inotify watch for cgroup cleanup */
@@ -841,6 +845,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 +856,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 +894,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;
}
+4 -9
View File
@@ -37,7 +37,7 @@
#define CDIM plain ? "" : "\e[2m"
#define CRST plain ? "" : "\e[0m"
#define CLREOL plain ? "" : "\e[K" /* Clear to end of line */
#define CLREOL plain ? "" : "\e[K" /* Clear to end of line */
#define NONE " "
#define PIPE plain ? "| " : "│ "
@@ -630,14 +630,9 @@ int cgroup_tree(char *path, char *pfx, int mode, int pos)
strlcat(row, ++i == num ? END : FORK, rlen);
snprintf(proc, sizeof(proc), " %d %s", pid, cmdline);
if (plain) {
strlcat(row, proc, rlen);
} else {
strlcat(row, CDIM, rlen);
strlcat(row, proc, rlen);
strlcat(row, CRST, sizeof(row));
}
strlcat(row, CDIM, rlen);
strlcat(row, proc, rlen);
strlcat(row, CRST, sizeof(row));
printf("\r%s%s\n", row, CLREOL);
pos++;
+5 -4
View File
@@ -37,6 +37,11 @@
#include "finit.h"
#include "log.h"
/* clone3() was added in Linux 5.3, syscall number 435 on all archs */
#ifndef __NR_clone3
#define __NR_clone3 435
#endif
/* Clone3 flags from linux/sched.h */
#ifndef CLONE_INTO_CGROUP
#define CLONE_INTO_CGROUP 0x200000000ULL
@@ -57,11 +62,7 @@ struct clone3_args {
uint64_t cgroup; /* Cgroup file descriptor */
} __attribute__((aligned(8)));
#ifdef __NR_clone3
static int use_clone3 = 1;
#else
static int use_clone3 = 0;
#endif
int has_clone3(void)
{
+15 -11
View File
@@ -121,9 +121,6 @@ static uev_t etcw;
static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_change_list);
static char *path;
static char *shell;
static int parse_conf(char *file, int is_rcsd);
static void drop_changes(void);
@@ -377,8 +374,6 @@ void conf_parse_cmdline(int argc, char *argv[])
fstab = strdup(ptr);
finit_conf = strdup(FINIT_CONF);
finit_rcsd = strdup(FINIT_RCSD);
path = getenv("PATH");
shell = getenv("SHELL");
for (int i = 1; i < argc; i++)
parse_arg(argv[i]);
@@ -404,13 +399,9 @@ void conf_reset_env(void)
free(node);
}
if (path)
setenv("PATH", path, 1);
else
if (!getenv("PATH"))
setenv("PATH", _PATH_STDPATH, 1);
if (shell)
setenv("SHELL", shell, 1);
else
if (!getenv("SHELL"))
setenv("SHELL", _PATH_BSHELL, 1);
setenv("LOGNAME", "root", 1);
setenv("USER", "root", 1);
@@ -745,8 +736,21 @@ void conf_parse_cond(svc_t *svc, char *cond)
return;
}
/*
* The '~' prefix means a reload of the upstream service is
* propagated to this service -- it will be reloaded (SIGHUP)
* or restarted (noreload), not just paused and resumed.
* Syntax: <!~pid/foo,~pid/bar> or <~pid/foo>
*
* Strip '~' from each condition and set the service-level
* flag. This allows '~' on any condition in the list.
*/
svc->cond[0] = 0;
for (i = 0, c = strtok(ptr, ","); c; c = strtok(NULL, ","), i++) {
if (c[0] == '~') {
c++;
svc->flux_reload = 1;
}
devmon_add_cond(c);
if (i)
strlcat(svc->cond, ",", sizeof(svc->cond));
+20 -1
View File
@@ -75,6 +75,7 @@ static void drop_node(struct dev_node *node)
void devmon_add_cond(const char *cond)
{
struct dev_node *node;
char path[PATH_MAX];
if (!cond || strncmp(cond, "dev/", 4)) {
// dbg("no match %s", cond ?: "<NIL>");
@@ -103,16 +104,34 @@ void devmon_add_cond(const char *cond)
node->refcnt = 1;
TAILQ_INSERT_TAIL(&dev_node_list, node, link);
snprintf(path, sizeof(path), "/%s", cond);
if (fexist(path))
cond_set(cond);
}
void devmon_del_cond(const char *cond)
{
if (!cond || strcmp(cond, "dev/"))
if (!cond || strncmp(cond, COND_DEV, strlen(COND_DEV)))
return;
drop_node(find_node(cond));
}
void devmon_reconf(void)
{
struct dev_node *node, *tmp;
char path[PATH_MAX];
TAILQ_FOREACH_SAFE(node, &dev_node_list, link, tmp) {
snprintf(path, sizeof(path), "/%s", node->name);
if (fexist(path))
cond_set(node->name);
else
cond_clear(node->name);
}
}
static int devmon_add_path(struct iwatch *iw, char *path)
{
char *ptr;
+1
View File
@@ -27,6 +27,7 @@
void devmon_add_cond(const char *cond);
void devmon_del_cond(const char *cond);
void devmon_reconf(void);
void devmon_init(uev_ctx_t *ctx);
#endif /* FINIT_DEVMON_H_ */
+6
View File
@@ -286,6 +286,12 @@ static void prepare_tty(char *tty, speed_t speed, const char *procname, struct r
char name[80];
int fd, dummy;
/* Set proper protections and ownership */
if (chown(tty, 0, 0))
logit(LOG_ERR, "failed chowning 0:0 %s: %s", tty, strerror(errno));
if (chmod(tty, 0620))
logit(LOG_ERR, "failed chmoding 620 %s: %s", tty, strerror(errno));
fd = open(tty, O_RDWR);
if (fd < 0) {
logit(LOG_ERR, "Failed opening %s: %s", tty, strerror(errno));
+32 -8
View File
@@ -39,6 +39,7 @@
#include <sys/sysmacros.h>
#endif
#include <sys/prctl.h>
#include <paths.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
@@ -60,8 +61,9 @@
#include "util.h"
#include "utmp-api.h"
uev_ctx_t *ctx = NULL; /* Main loop context */
svc_t *wdog = NULL; /* No watchdog by default */
uev_ctx_t *ctx; /* Main loop context */
svc_t *wdog; /* No watchdog by default */
char *arg0; /* Saved for setprocnm() */
/*
@@ -368,10 +370,9 @@ static void fs_finalize(void)
fs_mount("shm", "/dev/shm", "tmpfs", flags | MS_NODEV, "mode=1777");
}
/* Modern systems use /dev/pts */
if (!fismnt("/dev/pts")) {
/* Remount devpts with proper gid/mode now that /etc/group is available */
{
char opts[32];
int mode;
int gid;
gid = getgroup("tty");
@@ -379,11 +380,11 @@ static void fs_finalize(void)
gid = 0;
/* 0600 is default on Debian, use 0620 to get mesg y by default */
mode = 0620;
snprintf(opts, sizeof(opts), "gid=%d,mode=%d,ptmxmode=0666", gid, mode);
snprintf(opts, sizeof(opts), "gid=%d,mode=0620,ptmxmode=0666", gid);
makedir("/dev/pts", 0755);
fs_mount("devpts", "/dev/pts", "devpts", flags, opts);
fs_mount("devpts", "/dev/pts", "devpts",
flags | MS_REMOUNT, opts);
}
/* Needed on systems like Alpine Linux */
@@ -523,8 +524,19 @@ static void fs_init(void)
if (fismnt(fs[i].file))
continue;
/* Create mount point if it doesn't exist */
if (mkdir(fs[i].file, 0755) && errno != EEXIST)
warn("Failed creating mountpoint %s: %s", fs[i].file, strerror(errno));
fs_mount(fs[i].spec, fs[i].file, fs[i].type, 0, NULL);
}
/* Early devpts mount for plugins that need PTY access (e.g. plymouth) */
if (!fismnt("/dev/pts")) {
makedir("/dev/pts", 0755);
fs_mount("devpts", "/dev/pts", "devpts",
MS_NOSUID | MS_NOEXEC, "ptmxmode=0666");
}
}
@@ -610,6 +622,9 @@ int main(int argc, char *argv[])
{
uev_ctx_t loop;
/* Save argv for setprocnm() in child processes */
arg0 = argv[0];
/* user calling telinit or init */
if (getpid() != 1) {
if (geteuid() == 0)
@@ -624,6 +639,15 @@ int main(int argc, char *argv[])
return EX_NOPERM;
}
/*
* Sanity check critical environment variables. We need PATH
* and SHELL for early commands like mount in fs_init().
*/
if (!checkenv("PATH"))
setenv("PATH", _PATH_STDPATH, 1);
if (!checkenv("SHELL"))
setenv("SHELL", _PATH_BSHELL, 1);
/*
* Need /dev, /proc, and /sys for console=, remount and cgroups
*/
+1
View File
@@ -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
+91 -6
View File
@@ -32,6 +32,7 @@
#include <stdarg.h>
#include <sysexits.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
@@ -49,6 +50,7 @@
static pstyle_t progress_onoff = PROGRESS_DEFAULT;
static pstyle_t progress_style = PROGRESS_DEFAULT;
static char last_desc[512]; /* saved description for re-print after interruption */
#ifndef HOSTNAME_PATH
#define HOSTNAME_PATH "/etc/hostname"
@@ -335,6 +337,8 @@ void printv(const char *fmt, va_list ap)
len = print_timestamp(buf, sizeof(buf));
vsnprintf(&buf[len], sizeof(buf) - len, fmt, ap);
strlcpy(last_desc, &buf[len], sizeof(last_desc)); /* save for re-print */
if (progress_style == PROGRESS_CLASSIC)
cprintf("\r%s ", pad(buf, sizeof(buf), ".", sizeof(buf)));
else
@@ -343,24 +347,70 @@ void printv(const char *fmt, va_list ap)
void print(int rc, const char *fmt, ...)
{
char buf[ttcols];
size_t len;
if (progress_style == PROGRESS_SILENT)
return;
buf[0] = 0;
len = print_timestamp(buf, sizeof(buf));
if (fmt) {
va_list ap;
va_start(ap, fmt);
printv(fmt, ap);
vsnprintf(&buf[len], sizeof(buf) - len, fmt, ap);
va_end(ap);
strlcpy(last_desc, &buf[len], sizeof(last_desc));
} else if (rc >= 0 && last_desc[0]) {
/*
* No new description, but we have one saved from an earlier
* print(-1, ...) call. Re-print it so the final status is not
* left stranded if command output or a kernel message scrolled
* away the original description line.
*/
strlcpy(&buf[len], last_desc, sizeof(buf) - len);
} else {
buf[0] = 0;
}
if (rc < 0)
if (rc < 0) {
/* Pending state: show description with spinner, no final status yet */
if (!buf[0])
return;
if (progress_style == PROGRESS_CLASSIC)
cprintf("\r%s ", pad(buf, sizeof(buf), ".", sizeof(buf)));
else
cprintf("\r\e[K%s%s", status(3), buf);
return;
}
if (progress_style == PROGRESS_CLASSIC)
cprintf("%s\n", status(rc));
else
cprintf("\r%s\n", status(rc));
/*
* Final status. Emit description + status in a single cprintf() so
* both reach the console in one write(), preventing kernel messages or
* command output from splitting the description from its [ OK ]/[FAIL].
*
* status() returns a pointer to a static buffer, so we must copy the
* result for rc before calling status(3) — the second call overwrites
* the same buffer, corrupting the first result.
*/
last_desc[0] = 0;
if (buf[0]) {
char st[64];
strlcpy(st, status(rc), sizeof(st));
if (progress_style == PROGRESS_CLASSIC)
cprintf("\r%s %s\n", pad(buf, sizeof(buf), ".", sizeof(buf)), st);
else
cprintf("\r\e[K%s%s\r%s\n", status(3), buf, st);
} else {
if (progress_style == PROGRESS_CLASSIC)
cprintf("%s\n", status(rc));
else
cprintf("\r%s\n", status(rc));
}
}
void print_desc(char *action, char *desc)
@@ -374,6 +424,18 @@ int print_result(int fail)
return fail;
}
/*
* Reset console state and drain the output buffer before kernel takeover.
* Called just before reboot()/halt() to prevent ANSI escape codes from
* leaking into bootloader or early-kernel output.
*/
void print_exit(void)
{
tcdrain(STDERR_FILENO);
dprint(STDERR_FILENO, "\e[0m\e[?25h", 10); /* reset SGR, show cursor */
tcdrain(STDERR_FILENO);
}
void set_hostname(char **hostname)
{
FILE *fp;
@@ -527,6 +589,29 @@ int in_container(void)
return 0;
}
/**
* setprocnm - Set process name in argv[0] for ps/pidof
* @name: New process name to set
*
* Changes the process name by modifying argv[0]. This is particularly
* useful for compatibility with BusyBox's pidof which reads cmdline
* from /proc/pid/cmdline rather than /proc/pid/comm.
*/
void setprocnm(const char *name)
{
extern char *arg0;
size_t len;
if (!arg0 || !name)
return;
prctl(PR_SET_NAME, name, 0, 0, 0);
len = strlen(arg0);
memset(arg0, 0, len);
strlcpy(arg0, name, len + 1);
}
/**
* Local Variables:
* indent-tabs-mode: t
+13
View File
@@ -68,10 +68,12 @@ void printv (const char *fmt, va_list ap);
void print (int action, const char *fmt, ...) __attribute__ ((format (printf, 2, 3)));
void print_desc (char *action, char *desc);
int print_result (int fail);
void print_exit (void);
void set_hostname (char **hostname);
void networking (int updown);
int in_container (void);
void setprocnm (const char *name);
int complete (char *cmd, int pid);
int run (char *cmd, char *log);
@@ -82,6 +84,17 @@ pid_t run_sh (char *tty, int noclear, int nowait, struct rlimit rlimi
pid_t run_bg (char *cmd, char *args[]);
int run_parts (char *dir, char *cmd, const char *env[], int progress, int sysv);
static inline int checkenv(const char *env)
{
const char *val = getenv(env);
if (!val || !val[0])
return 0;
return 1;
}
static inline int dprint(int fd, const char *s, size_t len)
{
size_t loop = 3;
+125 -10
View File
@@ -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;
@@ -983,6 +1036,62 @@ static int svc_compare(svc_t *svc, char *arg)
return 0;
}
/*
* Escape a string for safe JSON output. Handles quotes, backslashes,
* and control characters. Returns pointer to static buffer.
*/
static char *json_escape(const char *str)
{
static char buf[1024];
char *ptr = buf;
size_t left = sizeof(buf) - 1;
if (!str)
return "";
while (*str && left > 1) {
char c = *str++;
switch (c) {
case '"':
case '\\':
*ptr++ = '\\';
*ptr++ = c;
left -= 2;
break;
case '\n':
*ptr++ = '\\';
*ptr++ = 'n';
left -= 2;
break;
case '\r':
*ptr++ = '\\';
*ptr++ = 'r';
left -= 2;
break;
case '\t':
*ptr++ = '\\';
*ptr++ = 't';
left -= 2;
break;
default:
if ((unsigned char)c < 0x20) {
/* Other control chars: use \uXXXX */
int n = snprintf(ptr, left, "\\u%04x", (unsigned char)c);
ptr += n;
left -= n;
} else {
*ptr++ = c;
left--;
}
break;
}
}
*ptr = '\0';
return buf;
}
static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
{
long now = jiffies();
@@ -998,14 +1107,16 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
fprintf(fp,
"%s"
"%s{\n"
"%s \"identity\": \"%s\",\n"
"%s \"description\": \"%s\",\n"
"%s \"identity\": \"%s\",\n",
prev ? ",\n" : indent, prev ? indent : "",
indent, svc_ident(svc, NULL, 0));
fprintf(fp,
"%s \"description\": \"%s\",\n",
indent, json_escape(svc->desc));
fprintf(fp,
"%s \"type\": \"%s\",\n"
"%s \"forking\": %s,\n"
"%s \"status\": \"%s\",\n",
prev ? ",\n" : indent, prev ? indent : "",
indent, svc_ident(svc, NULL, 0),
indent, svc->desc,
indent, svc_typestr(svc),
indent, svc->forking ? "true" : "false",
indent, svc_status(svc));
@@ -1027,15 +1138,17 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
}
fprintf(fp,
"%s \"origin\": \"%s\",\n"
"%s \"origin\": \"%s\",\n",
indent, svc->file[0] ? svc->file : "built-in");
svc_command(svc, buf, sizeof(buf), 0);
fprintf(fp,
"%s \"command\": \"%s\",\n",
indent, svc->file[0] ? svc->file : "built-in",
indent, svc_command(svc, buf, sizeof(buf), 0));
indent, json_escape(buf));
svc_environ(svc, buf, sizeof(buf), 0);
if (buf[0])
fprintf(fp,
"%s \"environment\": \"%s\",\n", indent, buf);
"%s \"environment\": \"%s\",\n", indent, json_escape(buf));
svc_cond(svc, buf, sizeof(buf), 0);
if (buf[0])
@@ -1474,7 +1587,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 +1768,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 }
+305
View File
@@ -0,0 +1,305 @@
/* 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 "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;
if (remove(fpath))
dbg("Failed to remove %s: %s", fpath, strerror(errno));
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 switch_root(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;
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 */
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 (fstat(fd, &newroot_st)) {
close(fd);
logit(LOG_ERR, "switch_root: cannot stat %s", newroot);
return -1;
}
close(fd);
/* Verify newroot is a mount point (different device than parent) */
fd = open("/", O_RDONLY | O_DIRECTORY);
if (fd < 0) {
logit(LOG_ERR, "switch_root: cannot open /");
return -1;
}
if (fstat(fd, &oldroot_st)) {
close(fd);
logit(LOG_ERR, "switch_root: cannot stat /");
return -1;
}
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;
}
/* 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;
iterate_proc(kill_cb, &signo);
do_usleep(500000); /* Give them 500ms */
signo = SIGKILL;
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 in new root. dup2() closes the old fds itself,
* so open() returns a fd > STDERR_FILENO that we can always close. */
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);
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:
*/
+3
View File
@@ -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"), \
+3
View File
@@ -59,6 +59,9 @@ void plugin_script_run(hook_point_t no);
int plugin_init (uev_ctx_t *ctx);
void plugin_exit (void);
void iterate_proc (int (*cb)(int, void *), void *data);
int switch_root (const char *newroot, const char *newinit);
#endif /* FINIT_PRIVATE_H_ */
/**
+321 -53
View File
@@ -25,13 +25,16 @@
#include "config.h" /* Generated by configure script */
#include <ctype.h> /* isblank() */
#include <grp.h> /* setgroups() */
#include <sched.h> /* sched_yield() */
#include <string.h>
#include <sys/reboot.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <linux/vt.h>
#include <net/if.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
@@ -386,6 +389,43 @@ static int lredirect(svc_t *svc)
return close(pipefd[1]);
}
/*
* Set up a controlling TTY for run/task/service.
* Opens the TTY device, dups to 0/1/2, resets termios to
* sane defaults, sets TERM env, and updates procname.
*/
static void svc_prepare_ctty(const char *tty, const char *procname, int log)
{
int fd, dummy;
fd = open(tty, O_RDWR);
if (fd < 0) {
logit(LOG_ERR, "Failed opening %s: %s", tty, strerror(errno));
_exit(1);
}
/* Acquire as controlling terminal for the session */
ioctl(fd, TIOCSCTTY, 0);
dup2(fd, STDIN_FILENO);
if (!log) {
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
}
/* Set default TERM */
if (ioctl(fd, VT_OPENQRY, &dummy) == -1)
setenv("TERM", "vt102", 1); /* likely a serial line */
else
setenv("TERM", "linux", 1);
/* Reset to sane defaults */
stty(fd, B0);
close(fd);
prctl(PR_SET_NAME, procname, 0, 0, 0);
}
/*
* Handle redirection of process output, if enabled
*/
@@ -495,14 +535,21 @@ static void compose_cmdline(svc_t *svc, char *buf, size_t len)
static void set_uid(uid_t uid, svc_t *svc)
{
#ifdef HAVE_LIBCAP
if (cap_setuid(uid)) {
err(1, "%s: failed cap_setuid(%d)", svc_ident(svc, NULL, 0), uid);
return;
}
/* After dropping privileges, set the specific capabilities we need */
/*
* Only use cap_setuid() if capabilities are configured,
* otherwise it would (by design) drop all capabilities,
* breaking root services
*/
if (svc->capabilities[0]) {
cap_iab_t cap_iab = cap_iab_from_text(svc->capabilities);
cap_iab_t cap_iab;
if (cap_setuid(uid)) {
err(1, "%s: failed cap_setuid(%d)", svc_ident(svc, NULL, 0), uid);
return;
}
/* After dropping privileges, set the specific capabilities we need */
cap_iab = cap_iab_from_text(svc->capabilities);
if (!cap_iab) {
err(1, "%s: failed parsing capabilities '%s'",
svc_ident(svc, NULL, 0), svc->capabilities);
@@ -515,11 +562,10 @@ static void set_uid(uid_t uid, svc_t *svc)
svc_ident(svc, NULL, 0));
}
cap_free(cap_iab);
}
#else
} else
#endif
if (setuid(uid))
err(1, "%s: failed setuid(%d)", svc_ident(svc, NULL, 0), uid);
#endif
}
static pid_t service_fork(svc_t *svc)
@@ -549,7 +595,21 @@ static pid_t service_fork(svc_t *svc)
#else
int uid = getuser(svc->username, &home);
int gid = getgroup(svc->group);
/* Check for invalid user/group - fail rather than silent fallback */
if (uid < 0) {
err(1, "%s: user '%s' not found, cannot start service",
svc_ident(svc, NULL, 0), svc->username);
return -1;
}
if (gid < 0) {
err(1, "%s: group '%s' not found, cannot start service",
svc_ident(svc, NULL, 0), svc->group);
return -1;
}
#endif
if (svc_is_tty(svc))
setprocnm("getty");
sched_yield();
@@ -560,6 +620,42 @@ static pid_t service_fork(svc_t *svc)
svc_ident(svc, NULL, 0), rlim2str(i));
}
#ifndef ENABLE_STATIC
/* Set supplementary groups from /etc/group and config */
{
gid_t supgids[NGROUPS_MAX];
int ngroups = NGROUPS_MAX;
int i, j, n = 0;
/* Get user's supplementary groups from /etc/group */
if (getgrouplist(svc->username, gid, supgids, &ngroups) >= 0)
n = ngroups;
/* Add explicitly configured supplementary groups */
for (i = 0; i < svc->num_supgroups && n < NGROUPS_MAX; i++) {
int g = getgroup(svc->supgroups[i]);
int found = 0;
if (g < 0) {
warn("%s: unknown supplementary group '%s'",
svc_ident(svc, NULL, 0), svc->supgroups[i]);
continue;
}
/* Skip if already in list from /etc/group */
for (j = 0; j < n; j++) {
if (supgids[j] == (gid_t)g) {
found = 1;
break;
}
}
if (!found)
supgids[n++] = g;
}
if (n > 0 && setgroups(n, supgids))
err(1, "%s: failed setgroups()", svc_ident(svc, NULL, 0));
}
#endif
/* Set desired user+group */
if (gid >= 0) {
if (setgid(gid))
@@ -570,8 +666,11 @@ static pid_t service_fork(svc_t *svc)
set_uid(uid, svc);
/* Set default path for regular users */
if (uid > 0)
if (uid > 0) {
setenv("PATH", _PATH_DEFPATH, 1);
setenv("USER", svc->username, 1);
setenv("LOGNAME", svc->username, 1);
}
if (home) {
setenv("HOME", home, 1);
if (chdir(home)) {
@@ -886,6 +985,14 @@ static int service_start(svc_t *svc)
sig_unblock();
/*
* If the stanza declared tty:<dev>, set up a controlling
* terminal on that device now that we're a session leader.
* Must run after setsid() to acquire the controlling TTY.
*/
if (!svc_is_tty(svc) && svc_has_ctty(svc))
svc_prepare_ctty(svc->log.ctty, svc->cmd, !!svc->log.enabled);
if (svc_is_runtask(svc))
status = exec_runtask(args[0], &args[1]);
else if (svc_is_tty(svc))
@@ -971,7 +1078,6 @@ static int service_run_script(svc_t *svc, char *script)
{
const char *id = svc_ident(svc, NULL, 0);
pid_t pid = service_fork(svc);
int status, rc;
if (pid < 0) {
err(1, "%s: failed forking off script %s", id, script);
@@ -987,6 +1093,8 @@ static int service_run_script(svc_t *svc, char *script)
};
char pidbuf[16];
redirect(svc);
snprintf(pidbuf, sizeof(pidbuf), "%d", svc->pid);
setenv("MAINPID", pidbuf, 1);
@@ -996,23 +1104,7 @@ static int service_run_script(svc_t *svc, char *script)
}
dbg("%s: script '%s' started as PID %d", id, script, pid);
if (waitpid(pid, &status, 0) == -1) {
warn("%s: failed calling script %s", id, script);
return -1;
}
rc = WEXITSTATUS(status);
if (WIFEXITED(status)) {
dbg("%s: script '%s' exited without signal, status: %d", id, script, rc);
} else if (WIFSIGNALED(status)) {
dbg("%s: script '%s' terminated by signal %d", id, script, WTERMSIG(status));
if (!rc)
rc = 1;
} else {
dbg("%s: script '%s' exited with status: %d", id, script, rc);
}
return rc;
return service_script_add(svc, pid, svc->killdelay);
}
/* Ensure we don't have any notify socket lingering */
@@ -1033,15 +1125,31 @@ static void service_notify_stop(svc_t *svc)
*/
static void service_cleanup(svc_t *svc)
{
char *fn;
char cond[MAX_COND_LEN];
/* PID collected, cancel any pending SIGKILL */
service_timeout_cancel(svc);
fn = pid_file(svc);
if (fn && remove(fn) && errno != ENOENT)
logit(LOG_CRIT, "Failed removing service %s pidfile %s",
svc_ident(svc, NULL, 0), fn);
/* Only clean up process' pidfile if managed by us */
if (svc_has_pidfile(svc)) {
char *fn = pid_file(svc);
if (remove(fn) && errno != ENOENT)
logit(LOG_CRIT, "Failed removing service %s pidfile %s",
svc_ident(svc, NULL, 0), fn);
}
/*
* Invalidate the pid/ condition for this service to ensure
* dependent services are properly stopped and restarted.
* Without this, the condition is only cleared asynchronously
* via inotify on pidfile removal, which may not trigger when
* the daemon fails to clean up its own pidfile, or when the
* service dies during a reload cycle and goes directly from
* RUNNING to HALTED (skipping STOPPING where cond_clear()
* is normally called).
*/
cond_clear(mkcond(svc, cond, sizeof(cond)));
service_notify_stop(svc);
@@ -1080,7 +1188,7 @@ int service_stop(svc_t *svc)
service_timeout_cancel(svc);
if (svc->stop_script[0]) {
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], calling stop:%s ...", id, svc->pid, svc->stop_script);
logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], calling stop:%s ...", id, svc->pid, svc->stop_script);
} else if (!svc_is_sysv(svc)) {
char *nm = pid_get_name(svc->pid, NULL, 0);
const char *sig = sig_name(svc->sighalt);
@@ -1095,10 +1203,10 @@ int service_stop(svc_t *svc)
}
dbg("Sending %s to pid:%d name:%s(%s)", sig, svc->pid, id, nm);
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], stopping, sending %s ...", id, svc->pid, sig);
logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], sending %s ...", id, svc->pid, sig);
} else {
compose_cmdline(svc, cmdline, sizeof(cmdline));
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], calling '%s stop' ...", id, svc->pid, cmdline);
logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], calling '%s stop' ...", id, svc->pid, cmdline);
}
/*
@@ -1218,7 +1326,7 @@ static int service_reload(svc_t *svc)
print_desc("Restarting ", svc->desc);
if (svc->reload_script[0]) {
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], calling reload:%s ...", id, svc->pid, svc->reload_script);
logit(LOG_CONSOLE | LOG_NOTICE, "Reloading %s[%d], calling reload:%s ...", id, svc->pid, svc->reload_script);
rc = service_run_script(svc, svc->reload_script);
} else if (svc->sighup) {
if (svc->pid <= 1) {
@@ -1226,8 +1334,8 @@ static int service_reload(svc_t *svc)
svc->start_time = svc->pid = 0;
goto done;
}
dbg("%s[%d], sending SIGHUP", id, svc->pid);
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], sending SIGHUP ...", id, svc->pid);
dbg("Reloading %s[%d], sending SIGHUP", id, svc->pid);
logit(LOG_CONSOLE | LOG_NOTICE, "Reloading %s[%d], sending SIGHUP ...", id, svc->pid);
rc = kill(svc->pid, SIGHUP);
if (rc == -1 && (errno == ESRCH || errno == ENOENT)) {
/* nobody home, reset internal state machine */
@@ -1384,6 +1492,26 @@ static void parse_env(svc_t *svc, char *env)
strlcpy(svc->env, env, sizeof(svc->env));
}
static void parse_caps(svc_t *svc, char *caps)
{
#ifdef HAVE_LIBCAP
cap_iab_t cap_iab;
cap_iab = cap_iab_from_text(caps);
if (!cap_iab) {
err(1, "%s: failed parsing capabilities '%s'", svc_ident(svc, NULL, 0), caps);
memset(svc->capabilities, 0, sizeof(svc->capabilities));
return;
}
cap_free(cap_iab);
strlcpy(svc->capabilities, caps, sizeof(svc->capabilities));
#else
(void)svc;
(void)caps;
#endif
}
/*
* the @cgroup argument can be, e.g., .system,mem.max:1234 or just the
* default group with some cfg, e.g., :mem.max:1234 as a side effect,
@@ -1714,11 +1842,12 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
char ident[MAX_IDENT_LEN];
char *ifstmt = NULL;
char *notify = NULL;
char *ctty = NULL;
struct tty tty = { 0 };
char *dev = NULL;
int respawn = 0;
int levels = 0;
int forking = 0, manual = 0, nowarn = 0;
int forking = 0, manual = 0, remain = 0, nowarn = 0;
int restart_max = SVC_RESPAWN_MAX;
int restart_tmo = 0;
unsigned oncrash_action = SVC_ONCRASH_IGNORE;
@@ -1786,6 +1915,8 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
forking = 1;
else if (MATCH_CMD(cmd, "manual:yes", arg))
manual = 1;
else if (MATCH_CMD(cmd, "remain:yes", arg))
remain = 1;
else if (MATCH_CMD(cmd, "restart:", arg)) {
if (MATCH_CMD(arg, "always", arg))
restart_max = -1;
@@ -1828,6 +1959,8 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
env = arg;
else if (MATCH_CMD(cmd, "caps:", arg))
caps = arg;
else if (MATCH_CMD(cmd, "tty:", arg))
ctty = arg;
/* catch both cgroup: and cgroup. handled in parse_cgroup() */
else if (MATCH_CMD(cmd, "cgroup", arg))
cgroup = arg;
@@ -1957,7 +2090,29 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
char *ptr = strchr(username, ':');
if (ptr) {
char *sup;
*ptr++ = 0;
/* Check for supplementary groups: group,sup1,sup2,... */
sup = strchr(ptr, ',');
if (sup) {
*sup++ = 0;
svc->num_supgroups = 0;
while (sup) {
char *next = strchr(sup, ',');
if (next)
*next++ = 0;
if (svc->num_supgroups >= MAX_NUM_SUPGROUPS) {
warn("%s: too many supplementary groups, max %d",
svc->cmd, MAX_NUM_SUPGROUPS);
break;
}
strlcpy(svc->supgroups[svc->num_supgroups], sup,
sizeof(svc->supgroups[0]));
svc->num_supgroups++;
sup = next;
}
}
strlcpy(svc->group, ptr, sizeof(svc->group));
}
strlcpy(svc->username, username, sizeof(svc->username));
@@ -2052,9 +2207,25 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
else
memset(svc->env, 0, sizeof(svc->env));
if (caps)
strlcpy(svc->capabilities, caps, sizeof(svc->capabilities));
parse_caps(svc, caps);
else
memset(svc->capabilities, 0, sizeof(svc->capabilities));
if (!svc_is_tty(svc) && ctty) {
char *dev = ctty;
/* NOTE: @console expands only to first console, not all */
if (tty_isatcon(ctty))
dev = tty_atcon();
dev = tty_canonicalize(dev);
if (dev)
strlcpy(svc->log.ctty, dev, sizeof(svc->log.ctty));
else
logit(LOG_WARNING, "%s: tty: %s not found, skipping ctty", svc_ident(svc, NULL, 0), ctty);
} else if (!svc_is_tty(svc)) {
memset(svc->log.ctty, 0, sizeof(svc->log.ctty));
}
if (file)
strlcpy(svc->file, file, sizeof(svc->file));
else
@@ -2069,6 +2240,18 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
memset(svc->ifstmt, 0, sizeof(svc->ifstmt));
svc->manual = manual;
svc->nowarn = nowarn;
/*
* remain:yes is not supported for bootstrap-only tasks. These
* tasks are deleted immediately after completion and their post:
* scripts never run. This is by design.
*/
if (remain && svc_is_runtask(svc) && !ISOTHER(levels, INIT_LEVEL)) {
logit(LOG_WARNING, "%s: remain:yes ignored for bootstrap-only tasks",
svc_ident(svc, NULL, 0));
remain = 0;
}
svc->remain = remain;
svc->respawn = respawn;
svc->forking = forking;
svc->restart_max = restart_max;
@@ -2092,15 +2275,20 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
/* only set forking based on pidfile if user supplied pid: option */
if (pid && svc->pidfile[0] == '!')
svc->forking = 1;
if (svc->restart_tmo == 0) {
if (svc_is_forking(svc))
svc->restart_tmo = 2000;
else
svc->restart_tmo = 1;
}
}
/* Set default restart_tmo for services and TTYs that can restart */
if (svc_is_daemon(svc) && svc->restart_tmo == 0) {
if (svc_is_forking(svc))
svc->restart_tmo = 2000;
else
svc->restart_tmo = 1;
}
/* TTYs need a longer default to throttle errors (e.g., missing device) */
if (svc_is_tty(svc) && svc->restart_tmo == 0)
svc->restart_tmo = 2000;
/* Set configured limits */
memcpy(svc->rlimit, rlimit, sizeof(svc->rlimit));
@@ -2296,7 +2484,21 @@ void service_update_rdeps(void)
if (!svc_is_noreload(svc))
continue; /* Yup, no need to stop start rdeps */
svc_mark_affected(mkcond(svc, cond, sizeof(cond)));
/*
* Clear the condition immediately, before service_step_all()
* runs. cond_clear() calls cond_update() which calls
* service_step() on all affected services right now. Those
* services see COND_OFF and get service_stop() called,
* transitioning to STOPPING_STATE before we ever send SIGTERM
* to this service. Without this, the condition is only cleared
* after the service dies, by which time reverse-dependencies
* may have already crashed due to the lost connection.
* See also: api.c do_reload() which does the same for direct
* 'initctl reload <svc>' calls.
*/
mkcond(svc, cond, sizeof(cond));
cond_clear(cond);
svc_mark_affected(cond);
}
}
@@ -2537,6 +2739,11 @@ static void service_retry(svc_t *svc)
return;
if (svc->respawn) {
/*
* Respawn services (TTYs) that exited with non-zero status
* have already been delayed in the SVC_RUNNING_STATE handler.
* Just restart now.
*/
dbg("%s crashed/exited, respawning ...", svc_ident(svc, NULL, 0));
svc_unblock(svc);
service_step(svc);
@@ -2747,8 +2954,12 @@ restart:
if (svc_is_removed(svc) && svc_has_cleanup(svc)) {
svc_set_state(svc, SVC_CLEANUP_STATE);
service_cleanup_script(svc);
} else
} else {
/* Unblock remain tasks after post script so they can restart */
if (svc_is_remain(svc))
svc_unblock(svc);
svc_set_state(svc, SVC_HALTED_STATE);
}
}
break;
@@ -2764,9 +2975,16 @@ restart:
break;
case SVC_DONE_STATE:
if (svc_is_changed(svc))
/* Remain tasks: always run post script when stopped, even if config changed */
if (svc_is_runtask(svc) && svc_is_remain(svc) && svc_is_stopped(svc)) {
if (svc_has_post(svc)) {
svc_set_state(svc, SVC_TEARDOWN_STATE);
service_post_script(svc);
} else
svc_set_state(svc, SVC_HALTED_STATE);
} else if (svc_is_changed(svc))
svc_set_state(svc, SVC_HALTED_STATE);
if (svc_is_runtask(svc) && svc_is_manual(svc) && enabled)
else if (svc_is_runtask(svc) && svc_is_manual(svc) && enabled)
svc_set_state(svc, SVC_WAITING_STATE);
break;
@@ -2899,6 +3117,8 @@ restart:
if (!svc->pid) {
if (svc_is_daemon(svc) || svc_is_tty(svc)) {
int rc = WEXITSTATUS(svc->status);
svc_restarting(svc); /* BLOCK_RESTARTING */
svc_set_state(svc, SVC_HALTED_STATE);
@@ -2913,6 +3133,18 @@ restart:
goto done;
}
/*
* Non-zero exit for respawn services (TTYs): delay retry
* to prevent busy-loop and rate-limit retries
*/
if (WIFEXITED(svc->status) &&
rc != 0) {
dbg("%s exited with error %d, delaying respawn ...",
svc_ident(svc, NULL, 0), rc);
service_timeout_after(svc, svc->restart_tmo, service_retry);
goto done;
}
dbg("respawning %s", svc_ident(svc, NULL, 0));
svc_unblock(svc);
break;
@@ -2983,6 +3215,19 @@ restart:
case COND_ON:
kill(svc->pid, SIGCONT);
svc_set_state(svc, SVC_RUNNING_STATE);
/*
* Propagate reload (~): upstream reloaded, so
* we must also reload/restart, not just resume.
*/
if (svc->flux_reload && !svc_is_changed(svc)) {
if (svc_is_noreload(svc))
service_stop(svc);
else
service_reload(svc);
break;
}
/* Reassert condition if we go from waiting and no change */
if (!svc_is_changed(svc)) {
if (svc->notify == SVC_NOTIFY_PID) {
@@ -3054,6 +3299,29 @@ void service_runtask_clean(void)
if (!svc_is_runtask(svc))
continue;
/* Remain tasks stay in DONE state if still valid in new runlevel */
if (svc_is_remain(svc) && svc->state == SVC_DONE_STATE) {
if (svc_in_runlevel(svc, runlevel) && !svc_is_changed(svc))
continue; /* Keep once flag, stay in DONE */
/* Config changed or leaving runlevel: stop, run post, restart */
svc->once = 0;
svc_stop(svc);
service_step(svc);
continue;
}
/*
* On reload (SIGHUP), only remain tasks (handled above) should
* have their once flag cleared and be restarted. Regular run/task
* that already ran in this runlevel must not run again.
*
* On runlevel change, continue below to reset once flag so tasks
* can run again in the new runlevel.
*/
if (sm_in_reload())
continue;
/* run/task declared with <!> */
if (svc->sighup)
svc->once = 1;
+15 -4
View File
@@ -77,7 +77,9 @@
#endif
#include <string.h> /* strerror() */
#include <sched.h>
#include <sys/mount.h>
#include <sys/reboot.h>
#include <sys/stat.h>
#include <sys/wait.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
@@ -206,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;
@@ -288,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);
@@ -301,6 +303,8 @@ void do_shutdown(shutop_t op)
int in_cont = in_container();
int signo = SIGTERM;
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: entered, op=%d\n", op);
if (!in_cont) {
/*
* On a PREEMPT-RT system, Finit must run as the highest prioritized
@@ -320,13 +324,15 @@ 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);
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: killing remaining processes ...\n");
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 */
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: plugin_exit() + cond_exit() ...\n");
plugin_exit();
/* We can no longer act on conditions, activate script runner */
@@ -349,12 +355,14 @@ void do_shutdown(shutop_t op)
for (int fd = 3; fd < 128; fd++)
close(fd);
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: vfork, child will reboot ...\n");
if (vfork()) {
/*
* Put PID 1 aside and let child perform reboot/halt
* kernel may exit child and we don't want to exit PID 1
* ... causing "aiii killing init" during reboot ...
*/
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: PID 1 returning from vfork\n");
return;
}
@@ -416,14 +424,17 @@ void do_shutdown(shutop_t op)
}
print(0, "Rebooting ...");
print_exit();
reboot(RB_AUTOBOOT);
} else if (op == SHUT_OFF) {
print(0, "Powering down ...");
print_exit();
reboot(RB_POWER_OFF);
}
/* Also fallback if any of the other two fails */
print(0, "Halting ...");
print_exit();
reboot(RB_HALT_SYSTEM);
}
+71 -4
View File
@@ -32,6 +32,7 @@
#include "cgroup.h"
#include "cond.h"
#include "conf.h"
#include "devmon.h"
#include "log.h"
#include "helpers.h"
#include "private.h"
@@ -62,9 +63,37 @@ static struct {
int skip_bootstrap; /* Set to skip bootstrap waiting */
} sm;
/* Shutdown trace: set SHUTDOWN_DEBUG to 1 in sm.h to enable */
#define shutdbg(fmt, args...) do { \
if (SHUTDOWN_DEBUG && (runlevel == 0 || runlevel == 6)) \
cprintf("shutdown[%c]: " fmt "\n", \
sm_rl2ch(runlevel), ##args); \
} while (0)
static uev_t console_watcher;
static int console_fd = -1;
/*
* Shutdown watchdog: if graceful service stop takes too long,
* force do_shutdown() directly from the event loop callback.
* Default timeout is 30 seconds, adjustable at build time.
*/
#ifndef SHUTDOWN_WDT_TIMEOUT
#define SHUTDOWN_WDT_TIMEOUT 30
#endif
static uev_t shutdown_wdt;
static void shutdown_wdt_cb(uev_t *w, void *arg, int events)
{
(void)w;
(void)arg;
(void)events;
logit(LOG_CONSOLE | LOG_CRIT, "Timed out (%d sec) waiting for services to stop, forcing shutdown.",
SHUTDOWN_WDT_TIMEOUT);
do_shutdown(halt);
}
/*
* Console input callback - handles Ctrl-C during bootstrap wait
*/
@@ -349,6 +378,7 @@ restart:
/* reload ? */
if (sm.reload) {
shutdbg("RUNNING got reload request!");
sm.reload = 0;
sm.state = SM_RELOAD_CHANGE_STATE;
}
@@ -361,12 +391,15 @@ restart:
/* Restore terse mode and run hooks before shutdown */
if (runlevel == 0 || runlevel == 6) {
/* Hide cursor, we're going down ... */
dprint(STDOUT_FILENO, "\033[?25l", 14);
shutdbg("entering CHANGE state, starting %d sec watchdog",
SHUTDOWN_WDT_TIMEOUT);
uev_timer_init(ctx, &shutdown_wdt, shutdown_wdt_cb,
NULL, SHUTDOWN_WDT_TIMEOUT * 1000, 0);
api_exit();
log_exit();
shutdbg("running HOOK_SHUTDOWN ...");
plugin_run_hooks(HOOK_SHUTDOWN);
shutdbg("HOOK_SHUTDOWN done");
}
dbg("Setting new runlevel --> %c <-- previous %c", sm_rl2ch(runlevel), sm_rl2ch(prevlevel));
@@ -386,12 +419,14 @@ restart:
}
/* Reset once flag of runtasks */
shutdbg("runtask_clean + step_all ...");
service_runtask_clean();
dbg("Stopping services not allowed in new runlevel ...");
sm.in_reload = 1;
service_step_all(SVC_TYPE_ANY);
shutdbg("CHANGE done, entering WAIT");
sm.state = SM_RUNLEVEL_WAIT_STATE;
break;
@@ -402,10 +437,13 @@ restart:
*/
svc = svc_stop_completed();
if (svc) {
shutdbg("WAIT, collecting %s[%d] ...", svc_ident(svc, NULL, 0), svc->pid);
dbg("Waiting to collect %s, cmd %s(%d) ...", svc_ident(svc, NULL, 0), svc->cmd, svc->pid);
break;
}
shutdbg("WAIT done, all services stopped");
/* Prev runlevel services stopped, call hooks before starting new runlevel ... */
dbg("All services have been stopped, calling runlevel change hooks ...");
plugin_run_hooks(HOOK_RUNLEVEL_CHANGE); /* Reconfigure HW/VLANs/etc here */
@@ -418,6 +456,7 @@ restart:
sm.in_reload = 0;
service_step_all(SVC_TYPE_ANY);
shutdbg("entering CLEAN");
sm.state = SM_RUNLEVEL_CLEAN_STATE;
break;
@@ -429,11 +468,15 @@ restart:
*/
svc = svc_clean_completed();
if (svc) {
shutdbg("CLEAN, waiting for %s[%d] (%s) ...",
svc_ident(svc, NULL, 0), svc->pid, svc_status(svc));
dbg("Waiting to collect post/cleanup script for %s, cmd %s(%d) ...",
svc_ident(svc, NULL, 0), svc->cmd, svc->pid);
break;
}
shutdbg("CLEAN done, all post/cleanup scripts collected");
/* Cleanup stale services */
svc_clean_dynamic(service_unregister);
@@ -449,9 +492,15 @@ restart:
* Tannhäuser Gate. All those .. moments .. will be lost in time, like
* tears ... in ... rain."
*/
if (runlevel == 0 || runlevel == 6)
if (runlevel == 0 || runlevel == 6) {
shutdbg("calling do_shutdown()");
uev_timer_stop(&shutdown_wdt);
do_shutdown(halt);
shutdbg("do_shutdown() returned to SM!");
}
shutdbg("entering RUNNING (reload=%d, newlevel=%d)",
sm.reload, sm.newlevel);
sm.state = SM_RUNNING_STATE;
break;
@@ -461,6 +510,9 @@ restart:
/* First reload all *.conf in /etc/finit.d/ */
conf_reload();
/* Handle remain tasks that need to run post script before restart */
service_runtask_clean();
/*
* Then, mark all affected service conditions as in-flux and
* let all affected services move to WAITING/HALTED
@@ -509,12 +561,27 @@ restart:
/* Cleanup stale services */
svc_clean_dynamic(service_unregister);
dbg("Step all services waiting for reasserted conditions ...");
service_step_all(SVC_TYPE_ANY);
dbg("Calling reconf hooks ...");
plugin_run_hooks(HOOK_SVC_RECONF);
dbg("Step all services waiting for reasserted conditions ...");
service_step_all(SVC_TYPE_ANY);
dbg("Update configuration generation of device conditions ...");
devmon_reconf();
dbg("Step all services waiting for reasserted conditions ...");
service_step_all(SVC_TYPE_ANY);
dbg("Update configuration generation of unmodified non-native services ...");
service_notify_reconf();
dbg("Step all services waiting for reasserted conditions ...");
service_step_all(SVC_TYPE_ANY);
dbg("Reconfiguration done");
sm.state = SM_RUNNING_STATE;
break;
+5
View File
@@ -24,6 +24,11 @@
#ifndef FINIT_SM_H_
#define FINIT_SM_H_
/* Set to 1 to trace shutdown (independent of 'initctl debug') */
#ifndef SHUTDOWN_DEBUG
#define SHUTDOWN_DEBUG 0
#endif
void sm_init (void);
void sm_step (void);
+4
View File
@@ -344,6 +344,10 @@ svc_t *svc_stop_completed(void)
for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) {
if (svc->state == SVC_STOPPING_STATE && svc->pid > 1)
return svc;
/* Also wait for remain tasks running their post script */
if (svc_is_remain(svc) && svc->state == SVC_TEARDOWN_STATE && svc->pid > 1)
return svc;
}
return NULL;
+8
View File
@@ -101,6 +101,7 @@ typedef enum {
#define MAX_STR_LEN 64
#define MAX_COND_LEN (MAX_ARG_LEN * 3)
#define MAX_USER_LEN 16
#define MAX_NUM_SUPGROUPS 4
#define MAX_NUM_FDS 64 /* Max number of I/O plugins */
#define MAX_NUM_SVC_ARGS 64
@@ -138,12 +139,14 @@ typedef struct svc {
svc_type_t type; /* Service, run, task, ... */
char protect; /* Services like dbus-daemon & udev by Finit */
char manual; /* run/task that require `initctl start foo` */
char remain; /* run/task: stay in DONE state, run post: on stop */
char nowarn; /* Skip or log warning if cmd missing or conflicts */
const int dirty; /* 0: unmodified, 1: modified */
const int removed;
int starting; /* ... waiting for pidfile to be re-asserted */
int runlevels;
int sighup; /* This service supports SIGHUP :) */
int flux_reload; /* Propagate reload from dependency, '~' prefix */
int forking; /* This is a service/sysv daemon that forks, wait for it ... */
svc_block_t block; /* Reason that this service is currently stopped */
char cond[MAX_COND_LEN];
@@ -173,6 +176,7 @@ typedef struct svc {
char file[64];
char prio[20];
char ident[20];
char ctty[32];
} log;
/* Only for TTY type services */
@@ -191,6 +195,8 @@ typedef struct svc {
/* Identity */
char username[MAX_USER_LEN];
char group[MAX_USER_LEN];
char supgroups[MAX_NUM_SUPGROUPS][MAX_USER_LEN];
int num_supgroups;
char capabilities[MAX_CMD_LEN];
/* Command, arguments and service description */
@@ -279,12 +285,14 @@ static inline int svc_is_tty (svc_t *svc) { return svc && SVC_TYPE_TTY
static inline int svc_is_runtask (svc_t *svc) { return svc && (SVC_TYPE_RUNTASK & svc->type);}
static inline int svc_is_forking (svc_t *svc) { return svc && svc->forking; }
static inline int svc_is_manual (svc_t *svc) { return svc && svc->manual; }
static inline int svc_is_remain (svc_t *svc) { return svc && svc->remain; }
static inline int svc_is_noreload (svc_t *svc) { return svc && (0 == svc->sighup && 0 == svc->reload_script[0]); }
static inline int svc_in_runlevel (svc_t *svc, int runlevel) { return svc && ISSET(svc->runlevels, runlevel); }
static inline int svc_has_pidfile (svc_t *svc) { return svc_is_daemon(svc) && svc->pidfile[0] != 0 && svc->pidfile[0] != '!'; }
static inline int svc_has_pre (svc_t *svc) { return svc->pre_script[0]; }
static inline int svc_has_post (svc_t *svc) { return svc->post_script[0]; }
static inline int svc_has_ctty (svc_t *svc) { return svc && svc->log.ctty[0]; }
static inline int svc_has_ready (svc_t *svc) { return svc->ready_script[0];}
static inline int svc_has_cleanup (svc_t *svc) { return svc->cleanup_script[0]; }
+299 -45
View File
@@ -34,6 +34,7 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
@@ -45,8 +46,29 @@
int debug;
int c_flag = 0;
int r_flag = 0;
int create_flag = 0;
int clean_flag = 0;
int remove_flag = 0;
#define MAX_EXCLUDES 32
static const char *exclude_prefixes[MAX_EXCLUDES];
static int num_excludes;
static void add_exclude_prefix(const char *prefix)
{
if (num_excludes < MAX_EXCLUDES)
exclude_prefixes[num_excludes++] = prefix;
}
static int excluded(const char *path)
{
for (int i = 0; i < num_excludes; i++) {
if (!strncmp(path, exclude_prefixes[i], strlen(exclude_prefixes[i])))
return 1;
}
return 0;
}
static int is_dir_empty(const char *path)
{
@@ -261,6 +283,122 @@ static void write_arg(FILE *fp, char *arg)
fputs(arg, fp);
}
/**
* Parse age string and return age in seconds.
* Returns 0 on invalid/empty age ("-" or NULL).
* Examples: "10d" = 864000, "1w" = 604800, "2h" = 7200
*/
static time_t parse_age(const char *age)
{
time_t val;
char *end;
if (!age || !strcmp(age, "-") || !strcmp(age, "0"))
return 0;
errno = 0;
val = strtoul(age, &end, 10);
if (errno || end == age)
return 0;
switch (*end) {
case 'w':
val *= 7;
/* fallthrough */
case 'd':
val *= 24;
/* fallthrough */
case 'h':
val *= 60;
/* fallthrough */
case 'm':
val *= 60;
/* fallthrough */
case 's':
case '\0':
break;
default:
return 0; /* unknown suffix */
}
return val;
}
/**
* Parse user string - supports both names and numeric UIDs.
* Returns UID on success, -1 on failure.
*/
static int parse_uid(const char *user)
{
long val;
if (!user || !user[0])
return 0;
/* Check if it's a numeric UID */
val = atonum(user);
if (val >= 0)
return val;
/* Not numeric, look up by name */
return getuser(user, NULL);
}
/**
* Parse group string - supports both names and numeric GIDs.
* Returns GID on success, -1 on failure.
*/
static int parse_gid(const char *group)
{
long val;
if (!group || !group[0])
return 0;
/* Check if it's a numeric GID */
val = atonum(group);
if (val >= 0)
return val;
/* Not numeric, look up by name */
return getgroup(group);
}
/* Globals for do_clean() callback - nftw doesn't support user data */
static time_t clean_age;
static time_t clean_now;
static int do_clean(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw)
{
int is_dir;
/* Skip the root directory itself */
if (ftw->level == 0)
return 0;
is_dir = (tflag == FTW_D || tflag == FTW_DP);
/*
* Conservative cleanup matching systemd-tmpfiles behavior:
* - Files: keep if ANY of atime, ctime, mtime is recent
* - Directories: keep if ANY of atime, mtime is recent (ctime
* excluded because cleanup itself updates directory ctime)
*/
if (clean_now - sb->st_mtime < clean_age)
return 0; /* mtime is too new, keep it */
if (clean_now - sb->st_atime < clean_age)
return 0; /* atime is too new, keep it */
if (!is_dir && clean_now - sb->st_ctime < clean_age)
return 0; /* ctime is too new, keep it (files only) */
if (remove(fpath) && errno != ENOENT && errno != EBUSY)
warn("Failed cleaning %s", fpath);
return 0;
}
/*
* The configuration format is one line per path, containing type, path,
* mode, ownership, age, and argument fields. The lines are separated by
@@ -295,6 +433,8 @@ static void tmpfiles(char *line)
errx(1, "Path name specifiers unsupported, skipping.");
return;
}
if (excluded(path))
return;
token = strtok(NULL, "\t ");
if (token) {
@@ -312,13 +452,12 @@ static void tmpfiles(char *line)
group = "root";
age = strtok(NULL, "\t ");
(void)age; /* unused atm. */
arg = strtok(NULL, "\n");
strc = stat(path, &st);
// file and directory removal logic
if (r_flag) {
if (remove_flag) {
switch (type[0]) {
case 'b':
case 'c':
@@ -349,7 +488,7 @@ static void tmpfiles(char *line)
break;
case 'X':
case 'x':
dbg("Unsupported x/X command, ignoring %s, no support for clean at runtime.", path);
/* handled by clean_flag (--clean) */
break;
case 'Z':
case 'z':
@@ -360,8 +499,34 @@ static void tmpfiles(char *line)
}
}
/* age-based cleanup logic */
if (clean_flag) {
time_t max_age = parse_age(age);
/* Only process if age is specified */
if (max_age > 0) {
switch (type[0]) {
case 'd':
case 'D':
case 'e':
if (!fisdir(path))
break;
clean_age = max_age;
clean_now = time(NULL);
nftw(path, do_clean, 20, FTW_DEPTH | FTW_PHYS);
break;
case 'x':
case 'X':
/* exclusion patterns - not yet implemented */
break;
default:
break;
}
}
}
// file & directory creation logic
if (c_flag) {
if (create_flag) {
switch (type[0]) {
case 'b':
rc = parse_mm(arg, &major, &minor);
@@ -409,26 +574,55 @@ static void tmpfiles(char *line)
rc = 0;
break;
case 'd':
case 'D':
case 'D': {
int uid, gid;
mode_t omask;
mkparent(path, 0755);
rc = mksubsys(path, mode ?: 0755, user, group);
omask = umask(0);
uid = parse_uid(user);
if (uid >= 0) {
gid = parse_gid(group);
if (gid < 0)
gid = 0;
rc = makedir(path, mode ?: 0755);
if (rc && errno == EEXIST)
rc = chmod(path, mode ?: 0755);
if (chown(path, uid, gid))
warn("Failed chown(%s, %d, %d)", path, uid, gid);
}
umask(omask);
break;
}
case 'e':
if (glob(path, GLOB_NOESCAPE, NULL, &gl))
break;
for (size_t i = 0; i < gl.gl_pathc; i++)
rc += mksubsys(gl.gl_pathv[i], mode ?: 0755, user, group);
for (size_t i = 0; i < gl.gl_pathc; i++) {
char *p = gl.gl_pathv[i];
int uid, gid;
/* e only adjusts existing directories */
if (!fisdir(p))
continue;
uid = parse_uid(user);
gid = parse_gid(group);
if (gid < 0)
gid = 0;
if (mode)
chmod(p, mode);
if (uid >= 0 && chown(p, uid, gid))
warn("Failed chown(%s, %d, %d)", p, uid, gid);
}
break;
case 'f':
case 'F':
mkparent(path, 0755);
if (type[1] == '+' || type[0] == 'F') {
/* f+/F will create or truncate the file */
if (!arg) {
rc = create(path, mode ?: 0644, user, group);
break;
}
fp = fopen(path, "w+");
} else {
/* f will create the file if it doesn't exist */
@@ -437,8 +631,20 @@ static void tmpfiles(char *line)
}
if (fp) {
int uid, gid;
write_arg(fp, arg);
rc = fclose(fp);
/* Apply mode and ownership */
if (mode)
chmod(path, mode);
uid = parse_uid(user);
gid = parse_gid(group);
if (gid < 0)
gid = 0;
if (uid >= 0 && chown(path, uid, gid))
warn("Failed chown(%s, %d, %d)", path, uid, gid);
}
break;
case 'l': /* Finit extension, like 'L' but only if target exists */
@@ -465,7 +671,10 @@ static void tmpfiles(char *line)
if (!strc) {
if (type[1] != '+')
break;
rmrf(path);
if (fisdir(path))
rmrf(path);
else
erase(path);
}
mkparent(path, 0755);
if (!arg) {
@@ -505,7 +714,7 @@ static void tmpfiles(char *line)
break;
case 'X':
case 'x':
dbg("Unsupported x/X command, ignoring %s, no support for clean at runtime.", path);
/* handled by clean_flag (--clean) */
break;
case 'Z':
opts = "-R";
@@ -532,45 +741,96 @@ static void tmpfiles(char *line)
warn("Failed %s operation on path %s", type, path);
}
static void process_file(const char *fn)
{
FILE *fp;
fp = fopen(fn, "r");
if (!fp) {
warn("Failed to open %s", fn);
return;
}
while (!feof(fp)) {
char *line;
line = fparseln(fp, NULL, NULL, NULL, FPARSELN_UNESCCOMM);
if (!line)
continue;
tmpfiles(line);
free(line);
}
fclose(fp);
}
static int usage(int rc)
{
fprintf(stderr,
"Usage: tmpfiles [COMMAND...]\n"
"Usage: tmpfiles [OPTIONS] [CONFIGFILE...]\n"
"\n"
"Commands:\n"
"Options:\n"
" -C, --clean Clean files and directories based on age\n"
" -c, --create Create files and directories\n"
" -d, --debug Show developer debug messages\n"
" -E Exclude /dev, /proc, /run, and /sys\n"
" --exclude-prefix=PFX Ignore rules for paths starting with PFX\n"
" -r, --remove Remove files and directories marked for removal\n"
" -h, --help This help text\n"
"\n"
"If no CONFIGFILE is specified, all *.conf files in the standard\n"
"tmpfiles.d directories are processed.\n"
"\n");
return rc;
}
enum {
OPT_EXCLUDE_PREFIX = 256,
};
int main(int argc, char *argv[])
{
struct option long_options[] = {
{ "create", 0, NULL, 'c' },
{ "debug", 0, NULL, 'd' },
{ "remove", 0, NULL, 'r' },
{ "help", 0, NULL, 'h' },
{ "clean", 0, NULL, 'C' },
{ "create", 0, NULL, 'c' },
{ "debug", 0, NULL, 'd' },
{ "exclude-prefix", 1, NULL, OPT_EXCLUDE_PREFIX },
{ "remove", 0, NULL, 'r' },
{ "help", 0, NULL, 'h' },
{ NULL, 0, NULL, 0 }
};
int c;
while ((c = getopt_long(argc, argv, "cdrh?", long_options, NULL)) != EOF) {
while ((c = getopt_long(argc, argv, "CcdrEh?", long_options, NULL)) != EOF) {
switch(c) {
case 'C':
clean_flag = 1;
break;
case 'c':
c_flag = 1;
create_flag = 1;
break;
case 'd':
debug = 1;
break;
case 'E':
add_exclude_prefix("/dev");
add_exclude_prefix("/proc");
add_exclude_prefix("/run");
add_exclude_prefix("/sys");
break;
case OPT_EXCLUDE_PREFIX:
add_exclude_prefix(optarg);
break;
case 'r':
r_flag = 1;
remove_flag = 1;
break;
case 'h':
@@ -582,12 +842,21 @@ int main(int argc, char *argv[])
}
}
if (c_flag + r_flag == 0) {
fprintf(stderr, "You need to specify at least one of --create or --remove.\n");
if (create_flag + clean_flag + remove_flag == 0) {
fprintf(stderr, "You need to specify at least one of --clean, --create, or --remove.\n");
return 1;
}
/* If config files specified on command line, process only those */
if (optind < argc) {
for (int i = optind; i < argc; i++)
process_file(argv[i]);
return 0;
}
/*
* No config files specified, process standard tmpfiles.d directories.
*
* Only the three last tmpfiles.d/ directories are defined in
* tmpfiles.d(5) as system search paths. Finit adds two more
* before that to have Finit specific ones sorted first, and
@@ -614,7 +883,6 @@ int main(int argc, char *argv[])
for (i = 0; i < gl.gl_pathc; i++) {
char *fn = gl.gl_pathv[i];
size_t j;
FILE *fp;
/* check for overrides */
for (j = i + 1; j < gl.gl_pathc; j++) {
@@ -627,26 +895,12 @@ int main(int argc, char *argv[])
if (!fn)
continue; /* skip, override exists */
fp = fopen(fn, "r");
if (!fp)
continue;
// info("Parsing %s ...", fn);
while (!feof(fp)) {
char *line;
line = fparseln(fp, NULL, NULL, NULL, FPARSELN_UNESCCOMM);
if (!line)
continue;
tmpfiles(line);
free(line);
}
fclose(fp);
process_file(fn);
}
globfree(&gl);
return 0;
}
/**
+38 -4
View File
@@ -180,16 +180,34 @@ char *str(char *fmt, ...)
return buf;
}
/*
* Return UID for the given user, with fallback to 'root' if user is unset.
* Fails with -1 only if user (incl. fallback) does not exist.
*/
int getuser(const char *username, char **home)
{
#ifdef ENABLE_STATIC
if (!username || !username[0])
username = "root";
if (home)
*home = "/"; /* XXX: Fixme */
return fgetint("/etc/passwd", "x:\n", username);
#else
struct passwd *usr;
const struct passwd *usr;
if (!username || (usr = getpwnam(username)) == NULL)
if (!username || !username[0]) {
usr = getpwuid(0);
if (!usr)
return -1;
if (home)
*home = usr->pw_dir;
return usr->pw_uid;
}
usr = getpwnam(username);
if (!usr)
return -1;
if (home)
@@ -198,14 +216,30 @@ int getuser(const char *username, char **home)
#endif
}
/*
* Return GID for the given group, with fallback to 'root' if group is unset.
* Fails with -1 only if user (incl. fallback) does not exist.
*/
int getgroup(const char *group)
{
#ifdef ENABLE_STATIC
if (!group || !group[0])
group = "root";
return fgetint("/etc/group", "x:\n", group);
#else
struct group *grp;
const struct group *grp;
if ((grp = getgrnam(group)) == NULL)
if (!group || !group[0]) {
grp = getgrgid(0);
if (!grp)
return -1;
return grp->gr_gid;
}
grp = getgrnam(group);
if (!grp)
return -1;
return grp->gr_gid;
+3
View File
@@ -10,6 +10,7 @@ EXTRA_DIST = skel/bin/busybox-x86_64.sha256 skel/sbin/service.sh skel/etc/env
skel/etc/rcS.d/S01abc.sh skel/etc/rcS.d/S02def.sh \
skel/cdrom/.empty skel/dev/shm/.empty skel/dev/pts/.empty \
skel/etc/inittab skel/etc/hostname skel/etc/fstab \
skel/etc/passwd skel/etc/group skel/etc/ld.so.conf \
skel/etc/init.d/rcS skel/etc/init.d/rcK skel/tmp/.empty \
skel/etc/finit.d/.empty skel/etc/finit.d/available/.empty \
skel/etc/finit.d/enabled/.empty \
@@ -31,6 +32,7 @@ EXTRA_DIST += add-remove-dynamic-service-sub-config.sh
EXTRA_DIST += bootstrap-crash.sh
EXTRA_DIST += cond-start-task.sh
EXTRA_DIST += crashing.sh
EXTRA_DIST += dep-chain-reload.sh
EXTRA_DIST += depserv.sh
EXTRA_DIST += devmon.sh
EXTRA_DIST += failing-sysv.sh
@@ -73,6 +75,7 @@ TESTS += add-remove-dynamic-service-sub-config.sh
TESTS += bootstrap-crash.sh
TESTS += cond-start-task.sh
TESTS += crashing.sh
TESTS += dep-chain-reload.sh
TESTS += depserv.sh
TESTS += devmon.sh
TESTS += failing-sysv.sh
+181
View File
@@ -0,0 +1,181 @@
#!/bin/sh
# Verify transitive dependency chain during reload and crash
#
# Four services in a chain: A → B → C → D, all using notify:pid.
# B and C are placed in separate sub-config files so we can use
# 'initctl touch' on them independently. B supports SIGHUP (reload),
# while C and D use '!' (noreload) and '~' (propagate reload),
# matching a common pattern in real-world setups (e.g., FRR routing
# daemons on Infix OS).
#
# Chain: A ← B <pid/A> ← C <!~pid/B> ← D <!~pid/C>
#
# Test 1 - touch C + reload (the "Infix" scenario):
# C is in the middle of the chain. After 'initctl touch svc_c.conf'
# + 'initctl reload':
# - A and B should be unaffected (same PID)
# - C is stopped/started (config was touched, noreload)
# - D must be restarted (reload propagated from C)
#
# Test 2 - crash (kill -9):
# When B is killed with SIGKILL the crash path (RUNNING → HALTED)
# bypasses STOPPING. Without cond_clear() in service_cleanup(),
# pid/B is never invalidated and C, D are never restarted.
#
# Test 3 - touch B + reload:
# B supports SIGHUP (no '!' prefix). After 'initctl touch
# svc_b.conf' + 'initctl reload':
# - A should be unaffected (same PID)
# - B is SIGHUP'd (same PID, config was touched)
# - C and D must be restarted (reload propagated, '~' prefix)
set -eu
TEST_DIR=$(dirname "$0")
test_teardown()
{
say "Running test teardown."
run "rm -f $FINIT_RCSD/svc_b.conf $FINIT_RCSD/svc_c.conf"
}
pidof()
{
texec initctl -j status "$1" | jq .pid
}
test_setup()
{
run "cat >> $FINIT_CONF" <<EOF
service log:stdout notify:pid name:svc_a serv -np -i svc_a -- Chain root
service log:stdout notify:pid <!~pid/svc_c> name:svc_d serv -np -i svc_d -- Needs C
EOF
run "cat >> $FINIT_RCSD/svc_b.conf" <<EOF
service log:stdout notify:pid <pid/svc_a> name:svc_b serv -np -i svc_b -- Needs A
EOF
run "cat >> $FINIT_RCSD/svc_c.conf" <<EOF
service log:stdout notify:pid <!~pid/svc_b> name:svc_c serv -np -i svc_c -- Needs B
EOF
}
# shellcheck source=/dev/null
. "$TEST_DIR/lib/setup.sh"
sep "Configuration"
run "cat $FINIT_CONF"
run "cat $FINIT_RCSD/svc_b.conf"
run "cat $FINIT_RCSD/svc_c.conf"
say "Reload Finit to start all services"
run "initctl reload"
say "Wait for full chain to start"
retry 'assert_status "svc_d" "running"' 10 1
run "initctl status"
run "initctl cond dump"
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Test 1: touch C (middle of chain, noreload) + reload
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
sep "Test 1: Touch C (middle) and global reload"
pid_a=$(pidof svc_a)
pid_b=$(pidof svc_b)
pid_c=$(pidof svc_c)
pid_d=$(pidof svc_d)
say "PIDs before: A=$pid_a B=$pid_b C=$pid_c D=$pid_d"
run "initctl touch svc_c.conf"
run "initctl reload"
say "Wait for chain to settle"
retry 'assert_status "svc_d" "running"' 15 1
run "initctl status"
run "initctl cond dump"
new_pid_a=$(pidof svc_a)
new_pid_b=$(pidof svc_b)
new_pid_c=$(pidof svc_c)
new_pid_d=$(pidof svc_d)
say "PIDs after: A=$new_pid_a B=$new_pid_b C=$new_pid_c D=$new_pid_d"
# shellcheck disable=SC2086
assert "A was not restarted" $new_pid_a -eq $pid_a
# shellcheck disable=SC2086
assert "B was not restarted" $new_pid_b -eq $pid_b
# shellcheck disable=SC2086
assert "C was restarted (touched)" $new_pid_c -ne $pid_c
# shellcheck disable=SC2086
assert "D was restarted (transitive dep)" $new_pid_d -ne $pid_d
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Test 2: crash (kill -9), bypasses STOPPING
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
sep "Test 2: Kill B with SIGKILL (bypasses STOPPING)"
pid_b=$(pidof svc_b)
pid_c=$(pidof svc_c)
pid_d=$(pidof svc_d)
say "PIDs before: B=$pid_b C=$pid_c D=$pid_d"
run "kill -9 $pid_b"
say "Wait for B to respawn and chain to settle"
retry 'assert_status "svc_d" "running"' 15 1
run "initctl status"
run "initctl cond dump"
new_pid_b=$(pidof svc_b)
new_pid_c=$(pidof svc_c)
new_pid_d=$(pidof svc_d)
say "PIDs after: B=$new_pid_b C=$new_pid_c D=$new_pid_d"
# shellcheck disable=SC2086
assert "B was restarted (crashed+respawn)" $new_pid_b -ne $pid_b
# shellcheck disable=SC2086
assert "C was restarted (transitive dep)" $new_pid_c -ne $pid_c
# shellcheck disable=SC2086
assert "D was restarted (transitive dep)" $new_pid_d -ne $pid_d
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Test 3: touch B (supports SIGHUP) + reload
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
sep "Test 3: Touch B (SIGHUP) and global reload"
pid_a=$(pidof svc_a)
pid_b=$(pidof svc_b)
pid_c=$(pidof svc_c)
pid_d=$(pidof svc_d)
say "PIDs before: A=$pid_a B=$pid_b C=$pid_c D=$pid_d"
run "initctl debug"
run "initctl touch svc_b.conf"
run "initctl reload"
sleep 2
run "initctl status"
say "Wait for chain to settle"
retry 'assert_status "svc_d" "running"' 15 1
run "initctl status"
run "initctl cond dump"
new_pid_a=$(pidof svc_a)
new_pid_b=$(pidof svc_b)
new_pid_c=$(pidof svc_c)
new_pid_d=$(pidof svc_d)
say "PIDs after: A=$new_pid_a B=$new_pid_b C=$new_pid_c D=$new_pid_d"
# shellcheck disable=SC2086
assert "A was not restarted" $new_pid_a -eq $pid_a
# shellcheck disable=SC2086
assert "B was not restarted (SIGHUP)" $new_pid_b -eq $pid_b
# shellcheck disable=SC2086
assert "C was restarted (transitive dep)" $new_pid_c -ne $pid_c
# shellcheck disable=SC2086
assert "D was restarted (transitive dep)" $new_pid_d -ne $pid_d
return 0
+12 -3
View File
@@ -5,6 +5,7 @@
# - bar must not start until foo is ready
# - bar must be stopped when foo goes down
# - bar must be restarted when foo is restarted
# - bar must not be restarted when foo is reloaded (per-service reload)
#
# Regression test bug #314, that bar is not in a restart loop when foo
# is stopped. Also, verify that changing between runlevels, where foo
@@ -49,8 +50,15 @@ test_one()
run "initctl status"
assert "bar is restarted" "$(texec initctl |grep bar | awk '{print $1;}')" != "$pid"
say "Verify bar is NOT restarted when foo is reloaded (per-service) ..."
retry 'assert_status "bar" "running"' 5 1
pid=$(texec initctl |grep bar | awk '{print $1;}')
run "initctl reload foo"
retry 'assert_status "bar" "running"' 5 1
assert "bar is NOT restarted on reload" "$(texec initctl |grep bar | awk '{print $1;}')" == "$pid"
# Wait for spice to be stolen by the Harkonnen
sleep 3
sleep 5
# bar should now have detected the loss of spice and be in restart
run "initctl status"
assert_status "bar" "restart"
@@ -61,8 +69,9 @@ test_one()
assert_status "bar" "waiting"
}
run "initctl debug"
sep
test_one "pid/foo"
sep
run "initctl debug"
test_one "service/foo/running"
#run "initctl debug"
test_one "service/foo/ready"
+5
View File
@@ -42,6 +42,11 @@ test_one()
mkdev "$node"
assert_status "serv" "running"
assert_cond "$cond"
run "initctl reload"
assert_status "serv" "running"
assert_cond "$cond"
rmdev "$node"
assert_status "serv" "waiting"
+18
View File
@@ -2,6 +2,14 @@
set -eu
echo "=== Finit Test Sysroot Setup ==="
echo "Date: $(date)"
echo "SYSROOT: $SYSROOT"
echo "top_builddir: $top_builddir"
echo "srcdir: $srcdir"
echo "================================"
echo
# shellcheck disable=SC2154
make -C "$top_builddir" DESTDIR="$SYSROOT" install
@@ -21,3 +29,13 @@ done
for conf in 10-hotplug.conf; do
find "$SYSROOT" -name $conf -delete
done
# Update dynamic linker cache for /usr/local/lib libraries
echo "Running ldconfig in sysroot: $SYSROOT"
echo "Contents of $SYSROOT/etc/ld.so.conf:"
cat "$SYSROOT/etc/ld.so.conf" || echo "Warning: ld.so.conf not found"
echo "Libraries in $SYSROOT/usr/local/lib:"
ls -la "$SYSROOT/usr/local/lib/" 2>/dev/null || echo "Warning: /usr/local/lib not found in sysroot"
ldconfig -v -r "$SYSROOT" || echo "Warning: ldconfig failed with exit code $?"
echo "Verifying ldconfig cache was created:"
ls -la "$SYSROOT/etc/ld.so.cache" || echo "Warning: ld.so.cache not created"
+1 -1
View File
@@ -52,4 +52,4 @@ done
echo "$pid" > /tmp/oldpid
#echo "PID $pid, kill -9 ..."
kill -9 "$pid"
kill -9 "$pid" 2>/dev/null || true
+9
View File
@@ -0,0 +1,9 @@
root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:
tty:x:5:
disk:x:6:
dialout:x:20:
nogroup:x:65534:
+3
View File
@@ -0,0 +1,3 @@
/usr/local/lib
/usr/lib
/lib
+5
View File
@@ -0,0 +1,5 @@
root:x:0:0:root:/root:/bin/sh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
+6
View File
@@ -325,6 +325,12 @@ int main(int argc, char *argv[])
#endif
}
}
if (melange) {
mine(melange);
vanish = -2;
}
if (do_pidfile > 0)
pidfile(NULL);
reloading = 0;
+1 -1
View File
@@ -8,7 +8,7 @@ d /var/lib/alarm 0755 - - -
d /var/lib/misc 0755 - - -
d /var/lib/urandom 0755 - - -
d /var/spool 0755 - - -
d /var/tmp 0777 - - -
d /var/tmp 1777 - - -
# Used by pam_console(8)
d /run/console 1777 - - -