mouse/src

Diff

ea9ff564e7ffe9727e0ca023

Containerfile

Mode 100644100644; object f78e30d3af17e6f70c5a6dee

@@ -18,6 +18,7 @@
         perl=5.42.2-r0 \
         python3=3.14.5-r0 \
         rustup=1.29.0-r0 \
+        rsync=3.4.3-r1 \
         openssl-dev=3.5.7-r0 \
         xz=5.8.3-r0
 
@@ -52,7 +53,11 @@
 
 FROM toolchain AS rootfs-builder
 
-RUN apk add --no-cache byacc=20260126-r0
+# util-linux's Meson setup requires a program named bison even when its only
+# enabled target, agetty, does not use the parser generator. NetBSD curses does
+# require Berkeley Yacc, and Alpine makes the two packages conflict.
+RUN apk add --no-cache byacc=20260126-r0 \
+    && ln -s /usr/bin/byacc /usr/local/bin/bison
 
 RUN MOUSE_SKIP_KERNEL=1 \
     MOUSE_SOURCE_DIR=/opt/mouse-sources \

Makefile

Mode 100644100644; object 3a8df4295e433848beab46aa

@@ -17,7 +17,7 @@
 	./scripts/run-qemu.sh
 
 check: image
-	./scripts/check-c2.sh
+	./scripts/check-c3.sh
 
 audit: image
 	@printf '%s\n' "static ELF audit passed during the container build"

PORTS.md

Mode 100644; object 83f3d597cc9c

@@ -1,0 +1,21 @@
+# OpenRC service contract for MOUSE ports
+
+A port may install an OpenRC service definition at `/etc/init.d/<name>`.
+`<name>` must match `[a-z][a-z0-9_]*`, and exactly one definition may exist for
+each name. The definition owns runtime dependencies, supervision, health, and
+shutdown behaviour through normal `openrc-run` facilities.
+
+A port must not install persistent runlevel links, invoke or depend on
+`rc-update`, edit `/etc/rc.conf`, or maintain another enablement database. The
+administrator enables an optional ports service only with
+`enable_<name>=YES` in `/etc/rc.conf`; `cheesed` validates that assignment
+against the installed definition and creates disposable runlevel membership
+under `/run` on the next boot.
+
+Services should run in the foreground under `supervise-daemon` when they need
+process supervision. Runtime `service <name> start|stop|restart|reload|status`
+actions never change next-boot enablement.
+
+The `ports/mouse-echo` payload is the acceptance example. It installs the
+`mouse_echo` definition and executable without runlevel state; the base image's
+explicit `enable_mouse_echo=YES` policy is solely responsible for starting it.

README.md

Mode 100644100644; object 0d330f2b91e4b0840cb95ae6

@@ -1,6 +1,6 @@
 # MOUSE source tree
 
-This repository builds the MOUSE C2 boot image: Linux 6.18.35 from pinned
+This repository builds the MOUSE C3 boot image: Linux 6.18.35 from pinned
 kernel.org source using MOUSE's own x86_64 configuration, a MOUSE-owned mini
 rootfs with ChimeraUtils and static OpenRC 0.63.3, and `cheesed` from the
 sibling repository as `/sbin/cheesed`.
@@ -8,7 +8,9 @@
 The mini rootfs is assembled from an explicit file list rather than inheriting
 another distribution's filesystem. The kernel, musl, NetBSD curses, `tcsh`, and
 the selected ChimeraUtils base commands are compiled locally from pinned source
-archives. No shipped runtime artifact comes from Alpine.
+archives. C3 adds a static util-linux `agetty` and non-PAM Shadow `login`, with
+libxcrypt supplying the static password-hashing interface. No shipped runtime
+artifact comes from Alpine.
 
 ## Build and boot
 
@@ -41,12 +43,13 @@
 verified through `sources.lock`. Before writing the initramfs, the build rejects
 any base ELF executable with an interpreter or dynamic `NEEDED` entry.
 
-`make check` requires `expect`. It boots enabled and disabled policy variants
-under QEMU, verifies strict `/etc/rc.conf` translation into disposable
-runlevels, required-service ordering, `supervise-daemon` restart exhaustion and
-administrative recovery, forced termination on the declared stop schedule,
-runtime-only `service` operations, the absence of `rc-update`, and orderly
-OpenRC shutdown before poweroff and reboot.
+`make check` requires `expect`. It boots five policy and failure variants under
+QEMU. The suite verifies the supervised serial login, root's `tcsh` login
+environment, strict `/etc/rc.conf` translation into disposable runlevels,
+enabled and disabled ports-service policy, missing dependencies and dependency
+cycles without partial starts, required-service recovery, restart exhaustion,
+forced termination, runtime-only `service` operations, and reverse-order
+shutdown.
 
 To select a container frontend explicitly:
 
@@ -64,7 +67,15 @@
 
 ```text
 Cheesed to meet you! v0.1.0
-cheesed: C1 bootstrap complete; cheesed is PID 1
-cheesed: enabled optional services: mouse_optional
+cheesed: C3 bootstrap complete; cheesed is PID 1
+cheesed: enabled optional services: mouse_echo
 cheesed: OpenRC default transition complete
+mouse login: root (automatic login)
+MOUSE 0.1.0 (x86_64)
+mouse:~#
 ```
+
+The checked-in development image uses `agetty --autologin root` so the QEMU
+acceptance suite is deterministic. It still enters the conventional
+`agetty` → `login` → `tcsh` chain; the root password remains locked in
+`/etc/shadow`.

ports/mouse-echo/mouse-echo.c

Mode 100644; object 276254786f3b

@@ -1,0 +1,41 @@
+#include <fcntl.h>
+#include <signal.h>
+#include <unistd.h>
+
+static const char ready_path[] = "/run/mouse-echo.ready";
+static const char term_path[] = "/run/mouse-echo.term";
+
+static void write_marker(const char *path, const char *contents, size_t length)
+{
+	int descriptor = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+
+	if (descriptor == -1)
+		return;
+	(void)write(descriptor, contents, length);
+	(void)close(descriptor);
+}
+
+static void handle_term(int signal_number)
+{
+	static const char marker[] = "SIGTERM\n";
+
+	(void)signal_number;
+	write_marker(term_path, marker, sizeof(marker) - 1);
+}
+
+int main(void)
+{
+	static const char marker[] = "ready\n";
+	struct sigaction action = {
+		.sa_handler = handle_term,
+	};
+
+	if (sigemptyset(&action.sa_mask) == -1)
+		return 1;
+	if (sigaction(SIGTERM, &action, NULL) == -1)
+		return 1;
+	write_marker(ready_path, marker, sizeof(marker) - 1);
+
+	for (;;)
+		pause();
+}

ports/mouse-echo/rootfs/etc/init.d/mouse_echo

Mode 100644; object ecc57ff91178

@@ -1,0 +1,17 @@
+#!/sbin/openrc-run
+
+description="MOUSE example ports service"
+supervisor=supervise-daemon
+command=/usr/libexec/mouse-echo
+respawn_delay=0
+respawn_max=3
+respawn_period=10
+retry="TERM/1/KILL/1"
+
+depend() {
+	need console_login
+}
+
+start_pre() {
+	rm -f /run/mouse-echo.term
+}

rootfs/etc/csh.login

Mode 100644100644; object 6d34b767b3162920f948a439

@@ -1,8 +1,4 @@
 umask 022
 setenv SHELL /bin/tcsh
 
-if ( -r /etc/motd ) then
-    cat /etc/motd
-endif
-
 cd

rootfs/etc/group

Mode 100644100644; object 0b0aed46da25421bb9eb2d66

@@ -1,2 +1,3 @@
 root:x:0:
+tty:x:5:
 uucp:x:14:

rootfs/etc/init.d/console_login

Mode 100644; object b72f9f217cd4

@@ -1,0 +1,14 @@
+#!/sbin/openrc-run
+
+description="MOUSE serial console login"
+supervisor=supervise-daemon
+command=/usr/bin/agetty
+command_args="--autologin root --login-program /usr/bin/login --noclear --keep-baud 115200,38400,9600 ttyS0 vt100"
+respawn_delay=1
+respawn_max=0
+retry="TERM/2/KILL/1"
+
+start_pre() {
+	checkpath --file --mode 0664 /run/utmp
+	checkpath --file --mode 0664 /var/log/wtmp
+}

rootfs/etc/init.d/mouse_optional

Mode 100644; object 173b05646a96

@@ -1,25 +1,0 @@
-#!/sbin/openrc-run
-
-description="MOUSE optional supervised proof service"
-supervisor=supervise-daemon
-command=/usr/libexec/mouse-c2-daemon
-respawn_delay=0
-respawn_max=3
-respawn_period=10
-retry="TERM/1/KILL/1"
-
-depend() {
-	need mouse_ready
-}
-
-start_post() {
-	checkpath --file /run/mouse-optional.started
-}
-
-start_pre() {
-	rm -f /run/mouse-c2-daemon.term
-}
-
-stop_post() {
-	rm -f /run/mouse-optional.started
-}

rootfs/etc/init.d/mouse_ready

Mode 100644; object d1ad77b846a4

@@ -1,15 +1,0 @@
-#!/sbin/openrc-run
-
-description="MOUSE required C1 proof service"
-
-start() {
-	ebegin "Marking the MOUSE base ready"
-	checkpath --file /run/mouse-ready.started
-	eend $?
-}
-
-stop() {
-	ebegin "Stopping the MOUSE readiness service"
-	rm -f /run/mouse-ready.started
-	eend $?
-}

rootfs/etc/login.defs

Mode 100644; object cf5da23ae2e6

@@ -1,0 +1,7 @@
+ENV_PATH PATH=/usr/bin
+ENV_SUPATH PATH=/usr/bin
+HUSHLOGIN_FILE .hushlogin
+LOGIN_TIMEOUT 60
+MOTD_FILE /etc/motd
+TTYGROUP tty
+TTYPERM 0600

rootfs/etc/rc.conf

Mode 100644100644; object e0086165deab627887674398

@@ -1,2 +1,2 @@
 # Persistent MOUSE service policy belongs in this file.
-enable_mouse_optional=YES
+enable_mouse_echo=YES

rootfs/etc/securetty

Mode 100644; object 360e9251bc33

@@ -1,0 +1,1 @@
+ttyS0

rootfs/etc/shadow

Mode 100644; object e0ec2c3f45b6

@@ -1,0 +1,1 @@
+root:!:1:0:99999:7:::

rootfs/usr/lib/mouse/runlevels/default/console_login

Mode 100644; object cb88ee1b983c

@@ -1,0 +1,1 @@
+Required default-runlevel membership for the C3 console login service.

rootfs/usr/lib/mouse/runlevels/default/mouse_ready

Mode 100644; object 63cade7eb2f2

@@ -1,1 +1,0 @@
-Required default-runlevel membership for the C1 readiness service.

scripts/build-image.sh

Mode 100755100755; object 764b6f114e8cf06cfb587e04

@@ -29,7 +29,8 @@
     "$staging/run" \
     "$staging/sys" \
     "$staging/usr/bin" \
-    "$staging/var"
+    "$staging/var/log" \
+    "$staging/var/mail"
 chmod 0755 "$staging"
 chmod 0700 "$staging/root"
 install -d -m 1777 "$staging/tmp"
@@ -43,11 +44,13 @@
 install -m 0755 "$cheesed_binary" "$staging/usr/bin/cheesed"
 
 cp -R "$repo_dir/rootfs/." "$staging/"
+cp -R "$repo_dir/ports/mouse-echo/rootfs/." "$staging/"
 find "$staging/usr/lib/mouse/runlevels" -name .keep -delete
 ln -s mouse-release "$staging/etc/os-release"
 chmod 0755 \
-    "$staging/etc/init.d/mouse_optional" \
-    "$staging/etc/init.d/mouse_ready"
+    "$staging/etc/init.d/console_login" \
+    "$staging/etc/init.d/mouse_echo"
+chmod 0600 "$staging/etc/shadow"
 ln -s /run/openrc/runlevels/current "$staging/etc/runlevels"
 
 "$script_dir/audit-static-base.sh" "$staging"

scripts/build-static-base.sh

Mode 100755100755; object 34ee7674690de03c5dfdc99b

@@ -63,6 +63,7 @@
 extract meson-1.9.1.tar.gz
 extract ninja-1.13.1.tar.gz
 extract pkgconf-2.5.1.tar.gz
+extract linux-6.18.35.tar.xz
 extract musl-1.2.6.tar.gz
 extract netbsd-curses-0.3.2.tar.gz
 extract attr-2.5.2.tar.gz
@@ -72,6 +73,9 @@
 extract chimerautils-15.0.3.tar.gz
 extract libcap-2.77.tar.xz
 extract openrc-0.63.3.tar.gz
+extract util-linux-2.42.2.tar.xz
+extract libxcrypt-4.5.2.tar.xz
+extract shadow-4.19.4.tar.xz
 
 patch -d "$source_build_dir/tcsh-TCSH6_24_16" -p1 \
     <"$repo_dir/patches/tcsh-gethost-native.patch"
@@ -116,6 +120,11 @@
     make -s DESTDIR="$sysroot" install
 )
 
+make -s -C "$source_build_dir/linux-6.18.35" \
+    ARCH=x86 \
+    headers_install \
+    INSTALL_HDR_PATH="$sysroot/usr"
+
 install -d -m 0755 "$sysroot/usr/include/sys"
 install -m 0644 "$source_dir/cdefs.h" "$sysroot/usr/include/sys/cdefs.h"
 install -m 0644 "$source_dir/queue.h" "$sysroot/usr/include/sys/queue.h"
@@ -137,6 +146,55 @@
 cc="$script_dir/mouse-cc"
 cxx="$script_dir/mouse-cxx"
 ranlib="$script_dir/mouse-ranlib"
+
+(
+    cd "$source_build_dir/libxcrypt-4.5.2"
+    CC="$cc" \
+        AR="$llvm_ar" \
+        RANLIB="$ranlib" \
+        ./configure \
+        --build="$build_triplet" \
+        --host=x86_64-linux-musl \
+        --prefix=/usr \
+        --disable-shared \
+        --enable-static \
+        --disable-obsolete-api \
+        --disable-symvers \
+        --enable-hashes=sha512crypt
+    make -s -j"$jobs"
+    make -s DESTDIR="$sysroot" install
+)
+
+(
+    cd "$source_build_dir/shadow-4.19.4"
+    CC="$cc" \
+        AR="$llvm_ar" \
+        RANLIB="$ranlib" \
+        PKG_CONFIG="$tools_dir/bin/pkgconf" \
+        PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig" \
+        PKG_CONFIG_SYSROOT_DIR="$sysroot" \
+        ./configure \
+        --build="$build_triplet" \
+        --host=x86_64-linux-musl \
+        --prefix=/usr \
+        --disable-shared \
+        --enable-static \
+        --disable-logind \
+        --disable-nls \
+        --disable-shadowgrp \
+        --without-audit \
+        --without-libpam \
+        --without-selinux \
+        --without-acl \
+        --without-attr \
+        --without-libbsd \
+        --without-skey \
+        --without-tcb \
+        --without-nscd
+    make -s -j"$jobs" -C lib
+    make -s -j"$jobs" -C src login
+    install -m 0755 src/login "$base_root/usr/bin/login"
+)
 
 (
     cd "$source_build_dir/netbsd-curses-0.3.2"
@@ -260,6 +318,23 @@
         "default_library = 'static'" \
         "prefer_static = true"
 } >"$cross_file"
+
+util_linux_build="$source_build_dir/util-linux-2.42.2/build"
+PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig" \
+PKG_CONFIG_SYSROOT_DIR="$sysroot" \
+    python3 "$meson_source" setup \
+    "$util_linux_build" \
+    "$source_build_dir/util-linux-2.42.2" \
+    --cross-file="$cross_file" \
+    --prefix=/usr \
+    --buildtype=release \
+    --default-library=static \
+    -Dauto_features=disabled \
+    -Dbuild-agetty=enabled
+PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig" \
+PKG_CONFIG_SYSROOT_DIR="$sysroot" \
+    python3 "$meson_source" compile -C "$util_linux_build" agetty
+install -m 0755 "$util_linux_build/agetty" "$base_root/usr/bin/agetty"
 
 (
     cd "$source_build_dir/libcap-2.77"
@@ -442,8 +517,8 @@
 
 install -d -m 0755 "$base_root/usr/libexec"
 "$cc" -O2 -Wall -Wextra -Werror \
-    "$repo_dir/support/mouse-c2-daemon.c" \
-    -o "$base_root/usr/libexec/mouse-c2-daemon"
+    "$repo_dir/ports/mouse-echo/mouse-echo.c" \
+    -o "$base_root/usr/libexec/mouse-echo"
 
 printf '%s\n' '#!/bin/sh' 'exec /usr/bin/tput clear' >"$base_root/usr/bin/clear"
 chmod 0755 "$base_root/usr/bin/clear"

scripts/check-c3.exp

Mode 100755; object 0e183b5cb48c

@@ -1,0 +1,220 @@
+#!/usr/bin/expect -f
+
+if {$argc != 2} {
+    puts stderr "usage: check-c3.exp RUNNER MODE"
+    exit 2
+}
+
+set runner [file normalize [lindex $argv 0]]
+set mode [lindex $argv 1]
+if {$mode ni {enabled disabled recovery missing cycle}} {
+    puts stderr "invalid C3 check mode: $mode"
+    exit 2
+}
+set timeout 45
+
+proc fail {message} {
+    puts stderr "C3 check failed: $message"
+    exit 1
+}
+
+proc await_exact {value description} {
+    expect {
+        -exact $value {
+            return
+        }
+        timeout {
+            fail "timed out waiting for $description"
+        }
+        eof {
+            fail "QEMU exited while waiting for $description"
+        }
+    }
+}
+
+proc await_regexp {value description} {
+    expect {
+        -re $value {
+            return
+        }
+        timeout {
+            fail "timed out waiting for $description"
+        }
+        eof {
+            fail "QEMU exited while waiting for $description"
+        }
+    }
+}
+
+proc await_prompt {} {
+    send -- "\r"
+    await_exact "mouse:~# " "the tcsh prompt"
+}
+
+proc sendline {line} {
+    send -- $line
+    send -- "\r"
+}
+
+proc await_clean_exit {action} {
+    expect {
+        eof {
+            set result [wait]
+            set status [lindex $result 3]
+            if {$status != 0} {
+                fail "QEMU exited with status $status after $action"
+            }
+        }
+        timeout {
+            fail "QEMU did not exit after $action"
+        }
+    }
+}
+
+proc poweroff {} {
+    send -- "kill -TERM 1\r"
+    await_exact "cheesed: received poweroff request" "the poweroff request"
+    await_exact "cheesed: OpenRC shutdown transition complete" "the shutdown transition"
+    await_clean_exit "poweroff"
+}
+
+log_user 0
+spawn -noecho $runner
+await_exact "cheesed: C3 bootstrap complete; cheesed is PID 1" "PID 1 confirmation"
+
+if {$mode eq "recovery"} {
+    await_regexp {required service console_login is not started: status command exited with status [0-9]+; entering recovery console} "the required-service failure"
+    await_regexp {cheesed: started emergency shell as PID [0-9]+} "the recovery shell"
+    await_exact "mouse:~# " "the recovery prompt"
+    sendline {sh -c 'set -- `cat /proc/$PPID/stat`; cat /proc/$4/comm'}
+    await_exact "cheesed\r" "the recovery shell parent"
+    await_exact "mouse:~# " "the prompt after the recovery parent check"
+    poweroff
+    puts "C3 required-service recovery check passed"
+    exit 0
+}
+
+if {$mode eq "disabled"} {
+    await_exact "cheesed: enabled optional services: (none)" "disabled ports policy"
+} else {
+    await_exact "cheesed: enabled optional services: mouse_echo" "enabled ports policy"
+}
+if {$mode eq "missing"} {
+    await_exact "Service 'mouse_echo' needs non existent service 'absent_port_dependency'" "the missing-dependency diagnostic"
+}
+await_exact "Starting console_login" "the required console service"
+if {$mode eq "cycle"} {
+    await_exact "ERROR: cannot start cycle_peer as mouse_echo would not start" "the dependency-cycle diagnostic"
+}
+await_exact "mouse login: root (automatic login)" "the automatic root login"
+await_exact "MOUSE 0.1.0 (x86_64)" "the login banner"
+
+if {$mode eq "enabled"} {
+    await_exact "cheesed: OpenRC default transition complete" "the default transition"
+} elseif {$mode eq "disabled"} {
+    await_exact "cheesed: OpenRC default transition complete" "the default transition"
+} else {
+    await_exact "cheesed: OpenRC default transition complete" "the default transition after rejecting the optional service"
+    await_regexp {optional service mouse_echo failed: status command exited with status [0-9]+} "the optional-service failure"
+}
+await_prompt
+
+sendline {id}
+await_exact "uid=0(root) gid=0(root) groups=0(root)\r" "the root login identity"
+await_exact "mouse:~# " "the prompt after the identity check"
+sendline {echo $LOGNAME $USER $SHELL}
+await_exact "root root /bin/tcsh\r" "the login environment"
+await_exact "mouse:~# " "the prompt after the environment check"
+sendline {tty}
+await_exact "/dev/ttyS0\r" "the serial login tty"
+await_exact "mouse:~# " "the prompt after the tty check"
+sendline {sh -c 'set -- `cat /proc/$PPID/stat`; cat /proc/$4/comm'}
+await_exact "supervise-daemo\r" "the supervised login parent"
+await_exact "mouse:~# " "the prompt after the login parent check"
+sendline {sh -c 'test "$(cat /proc/1/comm)" = cheesed && ! command -v rc-update >/dev/null 2>&1'; echo BASE_$status}
+await_exact "BASE_0\r" "cheesed as PID 1 with no rc-update command"
+await_exact "mouse:~# " "the prompt after the base contract check"
+
+if {$mode eq "missing"} {
+    sendline {sh -c 'test ! -e /run/mouse-echo.unexpected'; echo ORDERED_$status}
+    await_exact "ORDERED_0\r" "no partial start after a missing dependency"
+    await_exact "mouse:~# " "the prompt after the missing-dependency check"
+    poweroff
+    puts "C3 missing-dependency check passed"
+    exit 0
+}
+
+if {$mode eq "cycle"} {
+    sendline {sh -c 'test ! -e /run/mouse-echo.unexpected && test ! -e /run/cycle-peer.unexpected'; echo ORDERED_$status}
+    await_exact "ORDERED_0\r" "no partial start after a dependency cycle"
+    await_exact "mouse:~# " "the prompt after the dependency-cycle check"
+    poweroff
+    puts "C3 dependency-cycle check passed"
+    exit 0
+}
+
+sendline {service console_login status}
+await_exact "status: started" "the required console service status"
+await_exact "mouse:~# " "the prompt after the console status"
+
+if {$mode eq "disabled"} {
+    sendline {sh -c 'test ! -e /run/openrc/runlevels/current/default/mouse_echo'; echo DISABLED_$status}
+    await_exact "DISABLED_0\r" "the absent ports runlevel member"
+    await_exact "mouse:~# " "the prompt after the disabled membership check"
+    sendline {sh -c 'cksum /etc/rc.conf > /run/rc.conf.cksum'; service mouse_echo start}
+    await_exact "Starting mouse_echo" "the runtime-only ports service start"
+    await_exact "mouse:~# " "the prompt after the runtime start"
+    sendline {sh -c 'test "$(cksum /etc/rc.conf)" = "$(cat /run/rc.conf.cksum)"'; echo POLICY_$status}
+    await_exact "POLICY_0\r" "unchanged persistent policy"
+    await_exact "mouse:~# " "the prompt after the persistent-policy check"
+    sendline {service mouse_echo stop}
+    await_exact "Stopping mouse_echo" "the runtime-only ports service stop"
+    await_exact "mouse:~# " "the prompt after the runtime stop"
+    poweroff
+    puts "C3 disabled ports-service check passed"
+    exit 0
+}
+
+sendline {sh -c 'test -L /run/openrc/runlevels/current/default/mouse_echo'; echo ENABLED_$status}
+await_exact "ENABLED_0\r" "the generated ports runlevel member"
+await_exact "mouse:~# " "the prompt after the enabled membership check"
+sendline {sh -c 'i=0; while [ "$i" -lt 4 ]; do child=`cat /run/openrc/options/mouse_echo/child_pid`; kill -KILL "$child"; i=$((i + 1)); sleep 1; done'; echo EXHAUST_$status}
+await_exact "EXHAUST_0\r" "four supervised ports-service crashes"
+await_exact "mouse:~# " "the prompt after exhausting respawns"
+sendline {service mouse_echo status; echo FAILED_STATUS_$status}
+await_exact "status: failed" "the failed ports-service status"
+await_exact "FAILED_STATUS_32\r" "the failed status exit code"
+await_exact "mouse:~# " "the prompt after failed status"
+sendline {service mouse_echo restart}
+await_exact "Starting mouse_echo" "administrative ports-service recovery"
+await_exact "mouse:~# " "the prompt after administrative recovery"
+sendline {set stubborn=`cat /run/openrc/options/mouse_echo/child_pid`; service mouse_echo stop}
+await_exact "Stopping mouse_echo" "the forced ports-service stop"
+await_exact "mouse:~# " "the prompt after the forced stop"
+sendline {sh -c 'test -f /run/mouse-echo.term'; echo TERM_SEEN_$status}
+await_exact "TERM_SEEN_0\r" "the ports daemon observing SIGTERM"
+await_exact "mouse:~# " "the prompt after the SIGTERM check"
+sendline {sh -c '! kill -0 "$1" 2>/dev/null' sh "$stubborn"; echo FORCED_GONE_$status}
+await_exact "FORCED_GONE_0\r" "SIGKILL escalation for the ports daemon"
+await_exact "mouse:~# " "the prompt after forced termination"
+sendline {service mouse_echo start}
+await_exact "Starting mouse_echo" "the final ports-service start"
+await_exact "mouse:~# " "the prompt after the final start"
+
+sendline {exit}
+await_exact "logout\r" "the first login session ending"
+await_exact "mouse login: root (automatic login)" "the respawned console login"
+await_exact "MOUSE 0.1.0 (x86_64)" "the respawned login banner"
+await_exact "mouse:~# " "the respawned tcsh prompt"
+sendline {sh -c 'for stat in /proc/[0-9]*/stat; do set -- $(cat "$stat"); test "$3" != Z || exit 1; done'; echo ZOMBIES_$status}
+await_exact "ZOMBIES_0\r" "the absence of zombie processes"
+await_exact "mouse:~# " "the prompt after the zombie check"
+
+sendline {kill -TERM 1}
+await_exact "cheesed: received poweroff request" "the poweroff request"
+await_exact "Stopping mouse_echo" "the ports service stopping first"
+await_exact "Stopping console_login" "the console service stopping after its dependent"
+await_exact "cheesed: OpenRC shutdown transition complete" "the shutdown transition"
+await_clean_exit "poweroff"
+
+puts "C3 enabled base-integration check passed"

scripts/check-c3.sh

Mode 100755; object 016204f88b15

@@ -1,0 +1,81 @@
+#!/bin/sh
+set -eu
+
+script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+repo_dir=$(dirname "$script_dir")
+build_dir=${MOUSE_BUILD_DIR:-"$repo_dir/build"}
+image="$build_dir/mouse-initramfs.cpio.gz"
+
+for command in expect cpio gzip; do
+    if ! command -v "$command" >/dev/null 2>&1; then
+        printf '%s\n' "$command is required for the C3 integration check" >&2
+        exit 1
+    fi
+done
+
+if [ ! -f "$image" ]; then
+    printf '%s\n' "run 'make image' before checking MOUSE" >&2
+    exit 1
+fi
+
+temporary=$(mktemp -d "${TMPDIR:-/tmp}/mouse-c3-check.XXXXXX")
+trap 'rm -rf "$temporary"' EXIT HUP INT TERM
+base_root="$temporary/base"
+mkdir "$base_root"
+gzip -dc "$image" | (cd "$base_root" && cpio -id 2>/dev/null)
+
+pack_variant() {
+    name=$1
+    root=$2
+    output="$temporary/mouse-initramfs-$name.cpio.gz"
+    (
+        cd "$root"
+        find . -print |
+            LC_ALL=C sort |
+            cpio -o --format newc -R 0:0 2>/dev/null |
+            gzip -9
+    ) >"$output"
+    printf '%s\n' "$output"
+}
+
+run_variant() {
+    name=$1
+    root=$2
+    mode=$3
+    variant=$(pack_variant "$name" "$root")
+    MOUSE_INITRAMFS="$variant" \
+        "$script_dir/check-c3.exp" "$script_dir/run-qemu.sh" "$mode"
+}
+
+"$script_dir/check-c3.exp" "$script_dir/run-qemu.sh" enabled
+
+disabled_root="$temporary/disabled"
+cp -R "$base_root" "$disabled_root"
+printf '%s\n' \
+    '# Persistent MOUSE service policy belongs in this file.' \
+    'enable_mouse_echo=NO' >"$disabled_root/etc/rc.conf"
+run_variant disabled "$disabled_root" disabled
+
+recovery_root="$temporary/recovery"
+cp -R "$base_root" "$recovery_root"
+install -m 0755 \
+    "$repo_dir/tests/fixtures/console_login_broken" \
+    "$recovery_root/etc/init.d/console_login"
+run_variant recovery "$recovery_root" recovery
+
+missing_root="$temporary/missing"
+cp -R "$base_root" "$missing_root"
+install -m 0755 \
+    "$repo_dir/tests/fixtures/mouse_echo_missing" \
+    "$missing_root/etc/init.d/mouse_echo"
+run_variant missing "$missing_root" missing
+
+cycle_root="$temporary/cycle"
+cp -R "$base_root" "$cycle_root"
+install -m 0755 \
+    "$repo_dir/tests/fixtures/mouse_echo_cycle" \
+    "$cycle_root/etc/init.d/mouse_echo"
+install -m 0755 \
+    "$repo_dir/tests/fixtures/cycle_peer" \
+    "$cycle_root/etc/init.d/cycle_peer"
+run_variant cycle "$cycle_root" cycle

sources.lock

Mode 100644100644; object d178f4f1ef042920cd260135

@@ -16,3 +16,6 @@
 79721badcad1987dead9c3609eb4877ab9b58821c06bdacb824f2c8897c11f2a https://github.com/pkgconf/pkgconf/archive/refs/tags/pkgconf-2.5.1.tar.gz pkgconf-2.5.1.tar.gz
 897bc18b44afc26c70e78cead3dbb31e154acc24bee085a5a09079a88dbf6f52 https://mirrors.edge.kernel.org/pub/linux/libs/security/linux-privs/libcap2/libcap-2.77.tar.xz libcap-2.77.tar.xz
 f5bc2257f22ca7f0920a12698ad1e8f85a0f91a90e8e83106611514ea409b8ec https://github.com/OpenRC/openrc/archive/refs/tags/0.63.3.tar.gz openrc-0.63.3.tar.gz
+03a05d3adf9602ef128f2da05b84b3205ce60c351e5737c0370f74000679ce8a https://www.kernel.org/pub/linux/utils/util-linux/v2.42/util-linux-2.42.2.tar.xz util-linux-2.42.2.tar.xz
+71513a31c01a428bccd5367a32fd95f115d6dac50fb5b60c779d5c7942aec071 https://github.com/besser82/libxcrypt/releases/download/v4.5.2/libxcrypt-4.5.2.tar.xz libxcrypt-4.5.2.tar.xz
+ce57a313e315a0a7cb04a8f50cc20753e994e487bbe9b78a2a824ca75cb486c0 https://github.com/shadow-maint/shadow/releases/download/4.19.4/shadow-4.19.4.tar.xz shadow-4.19.4.tar.xz

support/mouse-c2-daemon.c

Mode 100644; object 81fea5db201d

@@ -1,41 +1,0 @@
-#include <fcntl.h>
-#include <signal.h>
-#include <unistd.h>
-
-static const char ready_path[] = "/run/mouse-c2-daemon.ready";
-static const char term_path[] = "/run/mouse-c2-daemon.term";
-
-static void write_marker(const char *path, const char *contents, size_t length)
-{
-	int descriptor = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
-
-	if (descriptor == -1)
-		return;
-	(void)write(descriptor, contents, length);
-	(void)close(descriptor);
-}
-
-static void handle_term(int signal_number)
-{
-	static const char marker[] = "SIGTERM\n";
-
-	(void)signal_number;
-	write_marker(term_path, marker, sizeof(marker) - 1);
-}
-
-int main(void)
-{
-	static const char marker[] = "ready\n";
-	struct sigaction action = {
-		.sa_handler = handle_term,
-	};
-
-	if (sigemptyset(&action.sa_mask) == -1)
-		return 1;
-	if (sigaction(SIGTERM, &action, NULL) == -1)
-		return 1;
-	write_marker(ready_path, marker, sizeof(marker) - 1);
-
-	for (;;)
-		pause();
-}

tests/fixtures/console_login_broken

Mode 100644; object c5fb7d794a02

@@ -1,0 +1,5 @@
+#!/sbin/openrc-run
+
+description="Broken required console fixture"
+command=/usr/bin/missing-getty
+

tests/fixtures/cycle_peer

Mode 100644; object a457b5655496

@@ -1,0 +1,12 @@
+#!/sbin/openrc-run
+
+description="Dependency cycle peer fixture"
+
+depend() {
+	need mouse_echo
+}
+
+start() {
+	checkpath --file /run/cycle-peer.unexpected
+}
+

tests/fixtures/mouse_echo_cycle

Mode 100644; object 45fc25eb057f

@@ -1,0 +1,12 @@
+#!/sbin/openrc-run
+
+description="Dependency cycle fixture"
+
+depend() {
+	need cycle_peer
+}
+
+start() {
+	checkpath --file /run/mouse-echo.unexpected
+}
+

tests/fixtures/mouse_echo_missing

Mode 100644; object 8cee1cc64853

@@ -1,0 +1,12 @@
+#!/sbin/openrc-run
+
+description="Missing dependency fixture"
+
+depend() {
+	need absent_port_dependency
+}
+
+start() {
+	checkpath --file /run/mouse-echo.unexpected
+}
+