PID-1 Boot Design
Date: 2026-04-24
Revision: 2026-04-25 — BSD-pivot (post iter 1-5 findings)
Status: Revised; iter 6 implementation pending against the new service set
Scope: First boot of zyginit as PID 1 on Hammerhead (hh-prototest VM), replacing the SMF-managed boot chain with a zyginit-managed BSD-style boot.
1. Goal
Boot hh-prototest with /sbin/init pointing at zyginit, bringing the system to a state where the operator can SSH in and use the box. Deliver a small BSD-style service set (≈15 services) that uses direct command invocation (ifconfig, dhcpagent) instead of supervising SMF-coupled control-plane daemons. Tested rollback path via ZFS Boot Environments.
1.1 Design revision: BSD pivot (2026-04-25)
The original 21-service design (preserved in §4 history at the bottom of this doc) tried to supervise the existing illumos control-plane daemons — dlmgmtd, ipmgmtd, inetd, rpcbind, fmd — directly. Iter 1-5 (rev 24 through rev 50) surfaced a fundamental issue: these daemons are tightly coupled to svc.configd for their persistent property store via libscf. Setting SMF_FMRI in the environment bypasses their refusal-to-start banner, but they then silently degrade because scf_handle_decode_fmri() fails (no /etc/svc/volatile/repository_door), leaving them running but useless — ipmgmtd doesn't open its door socket; ipadm consequently fails with "Could not open handle to library"; lo0 is never plumbed; rpcbind cascades into "could not find loopback transports"; the whole network stack is dead.
We had three choices:
- A: Run
configdunder zyginit — keeps the SMF property-store daemon alive, drops onlysvc.startd. Gets the daemons working but admits SMF is too entangled to fully escape. - B: Use
/lib/svc/method/*scripts as zyginit's exec.start — same effective coupling toconfigdas A; just hides it behind shell wrappers. - C: Pivot to BSD-style direct invocation — drop the configd-coupled daemons entirely. Use
/sbin/ifconfigdirectly to plumb interfaces and configure addresses. Use/sbin/dhcpagentdirectly for DHCP. Skipinetd/rpcbind/fmdfor the default service set.
We chose C. Hammerhead retains the BSD primitives that make this work:
/sbin/ifconfigexists and successfully plumbs interfaces without needingipmgmtd/etc/hostname.<if>files supported (BSD-style interface naming/config)/sbin/dhcpagentis callable directly/etc/init.d/,/etc/rc[0-3].d/directories present (we won't use them, but the convention is there)
Reference for the approach: IRIX 6.5's /etc/init.d/network script (os_research/irix-6517-src/eoe/cmd/initpkg/init.d/network) does exactly this — IFCONFIG=/usr/etc/ifconfig, ROUTE=/usr/etc/route, flat-file configuration in /etc/config/, no daemon-based registry. zyginit-on-Hammerhead becomes essentially a Reef rewrite of pre-SMF Solaris boot semantics, replacing what IRIX/Solaris-9 did with shell.
Lessons from iter 1-5 also drove these other concrete fixes that survive in v2:
- fd-setup at PID 1: kernel doesn't preopen 0/1/2; zyginit must
open(/dev/console)anddup2()onto 0/1/2 before any I/O (rev 45) - argv[0] in supervisor: must include the program name as argv[0] when execvp'ing (rev 44)
- root-fs ZFS/UFS dispatch:
mount -o remount,rw /errors on ZFS root; usezfs set readonly=off(rev 43) - Tier ordering enforcement: between tiers, wait for oneshots to STOPPED and daemons to RUNNING before starting next tier (rev 47)
- Persistent log dir:
/var/log/zyginit/(not/var/run) so logs survive reboot for diagnosis (rev 48) - Hammerhead path corrections:
/bin/hostname(not/usr/bin/),/sbin/swapadd(no-a),/sbin/dumpadm, etc. - cron FIFO is at
/etc/cron.d/FIFO, not/var/run/cron_fifo
A useful diagnostic capability also fell out: with the VM shut down, the BE's logs are readable from the dev host via qemu-nbd + zfsonlinux read-only import, then mounting hhtest/ROOT/zyginit to read /var/log/zyginit/*.log. This bypasses the GRUB-cant-pick-BE limitation on this Hammerhead version.
2. Non-goals
- SMF removal from the BE. Phase C, separate milestone. First boot leaves SMF binaries installed-but-unused. We just don't invoke them from zyginit's service set.
- Supervising configd-coupled daemons.
dlmgmtd,ipmgmtd,inetd,rpcbind,fmdare explicitly out of zyginit's default service set. They depend on a runningsvc.configdthat we no longer launch. They remain available on the BE for an admin who chooses to set up SMF separately. - Modern illumos datalink/IP management semantics. No
dladm show-link/ipadm show-addrintegration. Configuration is via flat files (/etc/hostname.<if>,/etc/defaultrouter,/etc/resolv.conf). For laptops with WiFi roaming, link aggregation, IPMP, or VNICs for zones, this design is insufficient — those workflows need the SMF stack. - On-demand network services (inetd-style). No
inetdin the default set. Services that historically ran underinetd(telnet, rsh, finger, etc.) are not started. SSH is direct. - NFS / RPC.
rpcbindnot started; no NFS in scope. - Fault management.
fmdnot started; FMA events are not collected. - Single-user mode / runlevels.
zygctl singleandzygctl multiuserremain stubs. - Crash-recovery re-adoption.
ct_ctl_adoptrequires descendant relationship; only PID-1 in-place re-exec is supported. /sbin/halt,/sbin/reboot,/sbin/poweroff,/usr/sbin/shutdownintegration. Continue to calluadmindirectly. Clean shutdown goes throughzygctl.- Name services (NSS), NTP, performance targets. Deferred.
3. Architecture
3.1 Components
| Component | Path | Type | Purpose |
|---|---|---|---|
zyginit |
/sbin/zyginit |
ELF binary | Init daemon (PID 1) |
zygctl |
/sbin/zygctl |
ELF binary | Admin CLI |
| Config tree | /etc/zyginit/ |
Directory | Service TOMLs, per-service script dirs, enabled.d/ symlink farm |
| Runtime tree | /var/run/zyginit/ |
Directory | Per-service log files, socket, transient state |
| Control socket | /var/run/zyginit.sock |
Unix socket | zygctl ↔ zyginit IPC |
3.2 Boot sequence at runtime
- Kernel execs
/sbin/init(symlink to/sbin/zyginit) - zyginit detects PID (
getpid() == 1) and enters PID-1 mode - zyginit scans
/etc/zyginit/enabled.d/for symlinks - zyginit parses each TOML referenced; builds in-memory
ServiceDefset - zyginit builds dependency graph via topological sort into tier ordering
- zyginit starts services tier by tier:
- Within a tier: sequential start (parallel start is a future optimization)
- Between tiers: wait for all members of tier N to reach terminal state (RUNNING for daemons, STOPPED+exit=0 for oneshots) before starting tier N+1
- zyginit opens event loop on: contract bundle fd, signal self-pipe, zygctl socket
- System is "up" when all enabled services have started successfully
3.3 PID-1 vs non-PID-1 behavior
| Condition | Trigger | Behavior |
|---|---|---|
| PID 1 | SIGTERM / SIGINT | Halt: stop services in reverse tier order, sync, uadmin(A_SHUTDOWN, AD_HALT, 0) |
| PID 1 | zygctl halt |
As above |
| PID 1 | zygctl reboot |
Stop services, sync, uadmin(A_SHUTDOWN, AD_BOOT, 0) |
| PID 1 | zygctl poweroff |
Stop services, sync, uadmin(A_SHUTDOWN, AD_POWEROFF, 0) |
| not PID 1 | SIGTERM / SIGINT | Stop services, return from main() (normal process exit) |
| not PID 1 | zygctl halt |
Stop services, exit |
| not PID 1 | zygctl reboot / poweroff |
Refuse ("not PID 1") — safety rail for dev/test |
4. Service set (BSD-pivot, v2)
15 zyginit services in enabled.d/ by default (counted across §4.1–§4.6). Replaces the 21-service v1 design (full v1 service table viewable in hg log -p for this file) by dropping the configd-coupled daemons in favor of direct command invocation.
4.1 Tier 0 — no dependencies
| Service | Type | Script | Stop |
|---|---|---|---|
root-fs |
oneshot | /etc/zyginit/root-fs/start.sh (ZFS or UFS root: zfs set readonly=off or mount -F ufs -o remount,rw /) |
/etc/zyginit/root-fs/stop.sh (matching readonly=on / remount,ro) |
4.2 Tier 1 — requires root-fs
| Service | Type | Script | Stop |
|---|---|---|---|
devfs |
oneshot | inline: /usr/sbin/devfsadm |
— |
swap |
oneshot | inline: /sbin/swapadd (no -a; reads /etc/vfstab) |
/etc/zyginit/swap/stop.sh (swap -d each active entry) |
crypto |
oneshot | /etc/zyginit/crypto/start.sh (cryptoadm refresh) |
— |
4.3 Tier 2 — requires tier 1
| Service | Type | Script | Stop |
|---|---|---|---|
filesystem |
oneshot | /etc/zyginit/filesystem/start.sh (mountall -l + zpool import -a + zfs mount -a) |
/etc/zyginit/filesystem/stop.sh (umountall -l + zpool sync) |
identity |
oneshot | /etc/zyginit/identity/start.sh (hostname from /etc/nodename) |
— |
sysconfig |
oneshot | /etc/zyginit/sysconfig/start.sh (coreadm, dumpadm, keymap, scheduler, rctl, rmtmpfiles) |
— |
4.4 Tier 3 — requires filesystem + identity
| Service | Type | Script | Stop |
|---|---|---|---|
network |
oneshot | /etc/zyginit/network/start.sh — direct ifconfig invocation, no ipmgmtd: plumb lo0, walk /etc/hostname.<if> files for each physical interface, ifconfig <if> dhcp (which forks dhcpagent directly) or static address per file content, install default route from /etc/defaultrouter if present |
/etc/zyginit/network/stop.sh (kill dhcpagent, ifconfig unplumb interfaces) |
Critical change from v1: no dlmgmtd or ipmgmtd services. ifconfig plumbs interfaces directly via DLPI. dhcpagent is invoked directly via ifconfig <if> dhcp start, not via ipadm -T dhcp. This matches IRIX 6.5's /etc/init.d/network pattern (IFCONFIG=/usr/etc/ifconfig, ROUTE=/usr/etc/route).
/etc/hostname.<if> file format (OpenBSD-inspired, illumos-translated)
Verified on hh-prototest: /sbin/ifconfig is fully operational without ipmgmtd (plumb, address assignment, link state — all work). The OpenBSD hostname.if(5) format is line-oriented with each line being ifconfig args. We adopt the same line-oriented convention but translate to illumos's ifconfig syntax, since OpenBSD-specific keywords like inet alias map to illumos's addif.
Line forms supported by zyginit's network/start.sh:
| Line | Action |
|---|---|
| (empty file or whitespace) | ifconfig <if> plumb up — interface up, no address |
# comment or blank line |
skipped |
dhcp |
ifconfig <if> plumb; ifconfig <if> dhcp start (best effort — see DHCP caveat) |
inet <addr>/<prefix> |
ifconfig <if> inet <addr>/<prefix> up |
inet <addr> netmask <mask> |
ifconfig <if> inet <addr> netmask <mask> up |
addif <addr>/<prefix> |
additional alias address (illumos addif); requires interface already plumbed by an earlier line |
inet6 <addr>/<prefix> |
ifconfig <if> inet6 <addr>/<prefix> up (deferred to post-first-boot) |
up / down |
set link state explicitly |
Examples:
# /etc/hostname.vioif0 — DHCP via dhcpagent (best effort without ipmgmtd)
dhcp
# /etc/hostname.vioif0 — static IPv4
inet 192.168.122.50/24
# /etc/hostname.vioif0 — static IPv4 + alias
inet 192.168.122.50/24
addif 192.168.122.51/24
Default route
/etc/defaultrouter (one IPv4 address per line). network/start.sh runs route -n add default <addr> for each. Optional — DHCP-acquired routes don't need this file.
DHCP caveat
dhcpagent is linked against libipadm.so.1, which talks to ipmgmtd's door for persistent state. Without ipmgmtd running:
dhcpagentitself runs (self-contained DHCP state machine)- DHCP request/response on the wire works (uses raw sockets, not libipadm)
- Applying the lease to the interface uses libipadm → ipmgmtd, which may degrade
Empirical test required on hh-prototest in iter 6 boot. If DHCP-via-ifconfig fails without ipmgmtd, fallback options:
- Manually invoke
dhcpagent -a -f(adopt mode + foreground) and configure interface from received lease via direct DLPI ioctl - Static IP only for the test VM (configure libvirt to have a known static address, set
/etc/hostname.vioif0toinet 192.168.122.50/24) - Patch dhcpagent / replace with dhclient if available
The implementation plan should boot-test with static IP first to derisk the rest of the boot, then attempt DHCP as a follow-up.
4.5 Tier 4 — requires network (configd-free system daemons)
All daemons. All use stop.method = "contract" (default). restart.on = "failure", delay = 5, max_retries = 3.
| Service | exec.start | Note |
|---|---|---|
syseventd |
/usr/lib/sysevent/syseventd |
Kernel event delivery; doesn't read SMF properties |
syslogd |
/usr/sbin/syslogd |
Reads /etc/syslog.conf directly; SMF-property reads degrade to defaults |
utmpd |
/usr/lib/utmpd |
Cleans stale utmpx entries; no configd dependency |
pfexecd |
/usr/lib/pfexecd |
RBAC profile-exec helper; reads /etc/security/, no configd |
4.6 Tier 5 — user-facing services
| Service | exec.start | Note |
|---|---|---|
cron |
/etc/zyginit/cron/start.sh |
wrapper: rm -f /etc/cron.d/FIFO; exec /usr/sbin/cron |
sshd |
/usr/lib/ssh/sshd -D |
-D keeps it foreground; reads /etc/ssh/sshd_config |
console-login |
/etc/zyginit/console-login/start.sh |
wrapper exec'ing ttymon with properly-quoted prompt |
4.7 Optional / installable — TOMLs ship but symlinks not in enabled.d/ by default
These TOMLs exist in services/hammerhead/ but are NOT enabled by default. An admin can enable them via zygctl enable <svc> on hardware/configurations where they make sense:
| Service | When to enable |
|---|---|
powerd |
Real hardware with /dev/srn (not VMs) |
fmd |
When fault management is wanted (requires configd workaround or future SMF-coexistence design) |
dlmgmtd / ipmgmtd / inetd / rpcbind |
Only after a separate "run minimal SMF infrastructure under zyginit" design is implemented |
4.8 Dropped from the v1 design (configd-coupled, requires separate design to revisit)
These daemons require svc.configd running for libscf calls to succeed. They are intentionally absent from the BSD-pivot v2 service set:
dlmgmtd— datalink management. Replaced by directifconfigplumbing. Drawback: nodladm show-linkqueries; persistent datalink state across reboots is via/etc/hostname.<if>instead.ipmgmtd— IP interface management. Replaced by directifconfig/routeinvocation. Drawback: noipadmqueries.inetd— on-demand service launching. Modern illumosinetdreads from configd. No replacement for first boot; can be re-added when needed (e.g., viaxinetdor by accepting configd dependency).rpcbind— only needed for NFS/RPC; deferred until NFS is in scope.fmd— fault management daemon; heavy SMF coupling. Deferred.
4.9 SMF infrastructure (subsumed; no replacement)
svc.startd,svc.configd— replaced by zyginit (or absent in v2)manifest-import,early-manifest-import— no XML manifestsboot-archive,boot-archive-update— SMF-specificlogadm-upgrade,update-man-index— admin/cron concerns, not init's- All 7 SMF milestones (virtual groupings) — zyginit's dependency graph replaces these
4.10 Currently-disabled SMF services — also not resurrected in zyginit
Mirrors v1: name-service-cache, rbac, process-security, auditd, NFS/DNS/LDAP stacks all stay disabled.
5. TOML schema usage
Uses the schema already documented in docs/DESIGN.md §"Service Definition Format". The following subset is in active use for the 21 services:
[service]
name = "..."
description = "..."
type = "daemon" | "oneshot"
[exec]
start = "command or /etc/zyginit/<svc>/start.sh"
[dependencies]
requires = ["..."]
after = ["..."]
# Daemons only:
[stop]
method = "contract" # default; may be omitted
signal = "TERM" # default; may be omitted
timeout = 60 # default; may be omitted
[restart]
on = "failure"
delay = 5
max_retries = 3
[contract]
param = ["inherit", "noorphan"]
fatal = ["hwerr"]
# Oneshots with reversal:
[exec]
stop = "/etc/zyginit/<svc>/stop.sh"
[restart]
on = "never"
Defaults and omissions:
- Omitted
exec.user/exec.group→ inherits from zyginit → root when PID 1. Explicit and documented so there's no ambiguity. - Omitted
[stop]block → uses contract-signal defaults (TERM, 60s timeout). - Omitted
[contract]block → uses template defaults fromsetup_template()incontract.reef.
Deferred schema fields (supported by zyginit, unused in first boot):
exec.user,exec.group,exec.working_directory,exec.environment— none of the 21 services need privilege drop or per-service envsecurity.*,limits.*— not implemented / not in first boot
contract.param = ["inherit", "noorphan"] note:
The NOORPHAN flag means that if zyginit-as-PID-1 dies without cleanly abandoning contracts, children receive SIGHUP. This is correct for PID-1 re-exec (kernel sees the same process, no SIGHUP fires). It is incompatible with a crash-recovery path that promotes a fresh zyginit PID — but such a path is an explicit non-goal per §2.
6. Method script conventions
All scripts live under /etc/zyginit/<svc>/start.sh (and optional stop.sh) unless the service is trivial enough for an inline TOML command.
Template:
#!/bin/zsh
# <service> — <one-line purpose>
# Invoked by zyginit via exec.start in /etc/zyginit/<service>.toml
set -e
set -u
# ... steps ...
exit 0
Conventions:
- Shebang:
#!/bin/zsh(consistent with Hammerhead's shell policy; zsh is the system default, ksh93 is being phased out) - Error handling:
set -e(POSIX-style — preferred oversetopt ERR_EXITfor cross-shell readability) +set -u - Non-interactive only: no
read, no tty-dependent behavior - Self-contained: no
sourceing of SMF helper scripts (/lib/svc/share/*.sh) - No SMF API calls: no
svcprop,svcadm,svccfg. Config input comes viaexec.environmentin the TOML. - Explicit
exit 0on success (not relying on implicit last-command exit) - Use
|| truedeliberately per-command where a specific failure is acceptable (e.g.,dumpadm -d /path || trueif the dump device doesn't exist yet) - Idempotent when the action allows: prefer
mount -o remount,rw /(idempotent) over rawmountcommands. Use "check then do" patterns for inherently non-idempotent operations (ipadm show-if lo0 2>/dev/null || ipadm create-if lo0).
Execution environment zyginit provides to scripts:
- Fresh process (fork + exec)
- Working dir:
/unless TOML setsexec.working_directory - Environment: minimal (PATH, HOME, etc.) plus whatever
exec.environmentspecifies - stdout/stderr: redirected to
/var/run/zyginit/log/<svc>.log, truncated per start - User: inherited from zyginit (root at PID 1) unless
exec.userset
7. Shutdown orchestration
Shutdown is a first-class concern, not deferred. Unclean shutdown can corrupt ZFS; we do it right from the first boot.
7.1 Shutdown triggers
| Source | Type |
|---|---|
zygctl halt |
halt |
zygctl reboot |
reboot |
zygctl poweroff |
poweroff |
| SIGTERM / SIGINT (PID 1) | halt (conservative default) |
| SIGTERM / SIGINT (not PID 1) | exit (integration test path) |
Shutdown type is communicated to zyginit via the socket protocol (for zygctl commands) or inferred from signal source.
7.2 Shutdown walk
- Set
shutdown_requested = true,shutdown_type = <type> - Break out of main event loop
- Walk services in reverse tier order (not insertion order)
- For each service in the tier:
- Daemons in
STATE_RUNNING:stop_service()→ SIGTERM via contract, wait for contract EMPTY event, markSTATE_STOPPED - Oneshots in
STATE_STOPPEDwithexec.stopdefined (or/etc/zyginit/<svc>/stop.shpresent): fork/exec the stop command; wait for exit - Oneshots without stop: skip (no reversal defined)
- Daemons in
- Wait for all services in tier N to reach
STATE_STOPPEDbefore moving to tier N-1 - After tier 0 is drained:
sync()(flush pending writes) - Call
uadmin(A_SHUTDOWN, <fcn>, 0):AD_HALT(0) — halt, operator decides next step physicallyAD_BOOT(1) — reboot immediatelyAD_POWEROFF(6) — ACPI/BMC chassis off
7.3 Stop scripts concretely required
| Service | stop.sh action |
|---|---|
root-fs |
mount -o remount,ro / (called after filesystem/stop.sh has umounted everything else) |
filesystem |
umountall -l |
swap |
swap -d each entry |
Other services either use the contract-signal daemon default or have nothing to reverse.
7.4 Required code changes
supervisor.reef:
- Extend shutdown path to walk tiers in reverse
- For each tier: first send stops to all daemons, wait for them to exit, then run stop scripts for oneshots that have them
- Currently
shutdown_servicesskipsSTATE_STOPPEDservices — this must change
main.reef:
- Track
shutdown_typevariable (halt / reboot / poweroff) - After
shutdown_services(): callsync(), thenzyginit_shutdown(<fcn>)
FFI: helpers.c and contract.reef (or new shutdown.reef):
int zyginit_shutdown(int fcn)→ wrapsuadmin(A_SHUTDOWN, fcn, 0)extern "C" fn zyginit_shutdown(fcn: int): int
Linux stub: contract_linux_stubs.c gets a matching stub returning -1 (should never actually fire on Linux).
zygctl gets three new subcommands: halt, reboot, poweroff. Each sends a specific command over the socket; zyginit sets shutdown_type accordingly.
zygctl also gets two stub subcommands: single, multiuser. Both print "not yet implemented — reserved for future runlevel support" and exit 1. Documented in zygctl --help so the surface is stable.
8. Rollout phases
8.1 Phase A — non-PID-1 dry run
Run zyginit on the live hh-prototest system alongside SMF, using a scratch config directory.
ZYGINIT_CONFIG_DIR=/tmp/zyginit-dry \
/path/to/zyginit
Goals:
- TOMLs parse cleanly
- Dependency graph builds without cycles; tier order matches the spec
- zyginit supervises one or two non-conflicting test daemons (e.g., a second sshd on port 2222)
- Clean shutdown via SIGTERM works without uadmin (non-PID-1 path)
Out of scope for Phase A:
- Destructive oneshots (filesystem, network, devfs) — they'd conflict with SMF-managed copies. Skip these in the dry run.
8.2 Phase B — PID-1 boot in a fresh Boot Environment
beadm create zyginitbeadm mount zyginit /mnt- Install into
/mnt:/mnt/sbin/zyginit,/mnt/sbin/zygctl/mnt/etc/zyginit/*.toml(21 files)/mnt/etc/zyginit/<svc>/start.sh+stop.shas specified above/mnt/etc/zyginit/enabled.d/symlinks for all 21- Preserve old init:
mv /mnt/sbin/init /mnt/sbin/init.smf - New init:
ln -s /sbin/zyginit /mnt/sbin/init
beadm umount zyginit; beadm activate zyginit; init 6
8.3 Phase C — SMF removal (deferred, separate milestone)
Optional follow-up once Phase B is stable:
- In the zyginit BE: remove
/lib/svc/bin/svc.startd,/lib/svc/bin/svc.configd,/lib/svc/method/*,/etc/svc/ - Activate, reboot, prove the system still works without SMF
- This is when "zyginit replaces SMF" is literally true.
8.4 Failure recovery ladder
In order of cost:
zygctl reboot→ pickhammerheadBE from GRUB menu (seconds)- If GRUB menu inaccessible: boot into rescue media / single-user from install media, mount original BE, activate it
- If the pool is damaged:
sudo cp hh-prototest-backup-2026-04-24-pre-zyginit-pid1.qcow2 hh-prototest.qcow2on the dev host (2-3 min with VM shut down) - Last resort: VM rebuild from scratch (~20 min)
Disk-level backup captured at rev 21. Recovery drill is part of §9.F.
9. Success criteria for Phase B
Literal checklist run after first boot into the zyginit BE.
A. Boot completes
- No
kernel panic: init died -
zyginit: entering event loopappears on console - Every tier finishes within 60s total
- Console login prompt appears on
/dev/console
B. Expected service states
- All 21 services present in
zygctl list - Tier 0-3 oneshots:
STOPPEDwithexit=0 - Tier 3-5 daemons:
RUNNING - No service in
MAINTENANCE
C. Services actually functional
-
ssh zygaena@192.168.122.197from dev host -
logger -p user.info test→ visible in/var/adm/messages(syslogd) -
dladm show-linklists physical interfaceup -
ipadm show-addrlists lo0 + physical interface with IPv4 addresses -
ping 192.168.122.1succeeds -
crontab -lruns for root -
zfs listworks,hhtestpool mounted
D. zygctl end-to-end
-
zygctl status cronshowsRUNNINGwith uptime > 0 -
zygctl stop cron→STOPPED, thenzygctl start cron→RUNNING -
zygctl log sshdreturns without error -
zygctl reloadsucceeds (SIGHUP path)
E. Clean shutdown
-
zygctl halt: reverse-tier-order stop output in console; system halted cleanly -
zygctl reboot: cycles; comes back via zyginit again -
zygctl poweroff: VM transitions toshut offin libvirt - After any zygctl shutdown:
zpool statusclean, no replay-required flags
F. Rollback confirmed
- 3 clean reboot cycles into the zyginit BE
- Reboot into original
hammerheadBE from GRUB → SMF comes up normally -
beadm activate hammerheadfrom zyginit BE → reboot → lands onhammerheadBE - Disk-level restore drill executed at least once (time to recovery measured)
G. Explicit non-requirements
- No boot-time performance gate (measured for baseline only)
- No stability soak
- No DNS / hostname resolution testing
- No NTP / time sync
- No name services (NSS beyond /etc/passwd, /etc/hosts, /etc/group)
- No
zygctl single/zygctl multiuserfunctionality (stubs only) - No SMF removal (Phase C)
10. Observability during test
- Console access via
virsh console hh-prototestfor live boot watching. /var/log/zyginit/<svc>.logper-service logs (rev 48 changed default from/var/run/zyginit/logto/var/log/zyginitso logs survive reboot).- Out-of-band log access: with VM shut down, mount the BE's qcow2 from the dev host:
Restore:virsh destroy hh-prototest sudo qemu-nbd -c /dev/nbd0 -r /var/lib/libvirt/images/hh-prototest.qcow2 sudo zpool import -R /tmp/hh-mnt -o readonly=on -N -f -d /dev/nbd0p2 hhtest sudo zfs mount hhtest/ROOT/zyginit # logs at /tmp/hh-mnt/var/log/zyginit/zfs umount,zpool export hhtest,qemu-nbd -d /dev/nbd0. Proven across 3 iterations. - Capture console screenshot at each iter for visual cross-check.
11. Explicit deferrals (captured for future work)
Each of the following is a known follow-up, documented here so future milestones inherit the boundary:
- configd-coupled daemons re-add: if
dlmgmtd/ipmgmtd/inetd/rpcbind/fmdare needed, design either a "minimal SMF infrastructure under zyginit" (configd as a zyginit-supervised daemon) or a per-daemon configd-bypass shim. - Runlevel targets:
zygctl single,zygctl multiuserstubs shipped; implementation needs a target-set schema extension (e.g.,service.runlevels = ["single", "multiuser"]). - Milestone-style virtual services: systemd-target equivalent; probably needed to implement runlevels cleanly.
/sbin/halt,/sbin/reboot,/sbin/poweroff,/usr/sbin/shutdownwrappers: replace with versions that route throughzygctl.init <n>runlevel compatibility: unsupported; use zygctl.- Crash-recovery re-adoption: architecturally not supported (illumos descendant requirement); documented.
- NOORPHAN revisit: current template is correct for PID-1 re-exec; revisit only if a non-exec crash-recovery goal is added.
- GC tuning under sustained uptime: track separately.
- SMF removal from disk: Phase C — once zyginit is stable, optionally remove
/lib/svc/,/etc/svc/from the BE entirely. - Boot performance targets: no gate.
- Additional services (resource limits, audit, DNS, NFS, zones): add as needed post-first-boot.
- Hostid persistence: first boot reads
/etc/hostid; generation logic (for first-ever boot) not yet implemented. - Modern datalink/IP semantics: BSD-pivot loses
dladm/ipadmquery interfaces. For zone hosts, laptops, multi-homed servers, this design is insufficient; revisit when those use cases are in scope.
12. Pre-requisites completed before this spec
- Contract probe: 16/16 FFI probes pass on hh-prototest (rev 19)
- contract.reef bugs fixed: P_CTID 14→13, ctl fd O_WRONLY (rev 18)
- Path rename:
/etc/zyginit.d/→/etc/zyginit/(rev 20, 21) - Signal handling fix: dead-code flag-clearing block removed (rev 15)
- Build split:
helpers.c/contract_linux_stubs.cseparated (rev 16) - Disk backup:
hh-prototest-backup-2026-04-24-pre-zyginit-pid1.qcow2captured on dev host
Pre-requisites added during iter 1-5 of v1 (kept in v2):
- fd-setup at PID 1 (rev 45):
setup_pid1_console()opens/dev/consoleand dup2's onto 0/1/2 before any I/O - argv[0] fix (rev 44): supervisor.start_service includes program name as argv[0]
- uadmin/sync wiring (rev 24-31): full PID-1 shutdown path
- Tier-ordering enforcement (rev 47): supervisor waits for tier N to settle before starting N+1
- Persistent log dir (rev 48):
/var/log/zyginit/default - root-fs ZFS/UFS dispatch (rev 43): correct readonly toggle per fstype
- zygctl shutdown commands (rev 33-34): halt/reboot/poweroff + single/multiuser stubs
- Path corrections in scripts (rev 49-50):
/bin/hostname,/bin/rm,/sbin/swapadd(no-a),/sbin/dumpadm, etc.
13. Open questions / defer to implementation
Most v1 open questions are now answered (uadmin constants verified; ipmgmtd path verified; ttymon argv conventions known). v2 open questions answered through 2026-04-25 hh-prototest probing:
- ✅
ifconfigcapability without ipmgmtd:ifconfig lo0:99 plumb,ifconfig lo0:99 inet 127.99.0.1 netmask 255.0.0.0 up, andifconfig lo0:99 unplumball worked silently while ipmgmtd was running.ifconfigoperates at the DLPI/kernel-ioctl layer for plumbing; ipmgmtd integration is for persistent state, not for runtime configuration. - ✅
ifconfig <if> dhcp <subcommand>syntax: per ifconfig(8) man page on hh-prototest, supported subcommands arestart | stop | drop | extend | inform | ping | release | status.ifconfig vioif0 dhcp statusreturned valid info while pinging the running dhcpagent on TCP 4999. - ✅
/etc/hostname.<if>parsing: Hammerhead'sifconfigdoes NOT read this file directly. The file is consumed by SMF method scripts. zyginit'snetwork/start.shwill parse the file itself (line-oriented format documented in §4.4). - ✅
/etc/defaultrouter: empty/absent on hh-prototest (DHCP supplies the route). Format: one IPv4 address per line.network/start.shreads when present and DHCP isn't in use.
Remaining for iter 6 to empirically verify:
- ⚠ DHCP-via-dhcpagent without ipmgmtd running: dhcpagent links libipadm which talks to ipmgmtd. Plan: try
ifconfig <if> dhcp startafter starting dhcpagent fresh under zyginit; if it works, ship as default; if it fails, ship static-IP-only and document. The test VM has a libvirt-supplied dnsmasq DHCP server on 192.168.122.0/24 — perfect environment to verify. - ⚠ First-boot fallback: if DHCP fails, our test config will use a static IP (e.g.,
inet 192.168.122.50/24) chosen outside libvirt's dnsmasq pool to avoid conflicts.