diskOS installer: initial public beta

Flashes the diskOS custom UI onto the FiiO Snowsky Disc over Ingenic mask-ROM USB,
building the image from your own stock firmware. Runs from source via install.sh.
This commit is contained in:
b0hemia
2026-08-26 15:26:14 +10:00
commit e0bc4785e9
109 changed files with 12625 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
#!/bin/sh
# diskOS first-boot installer. Runs at S97 - BEFORE S98FIIO launches the UI, and after
# S21mount_ubifs has mounted /usr/data. Installs the diskOS UI into /usr/data (which survives
# rootfs flashes) from - in deterministic precedence - (1) the copy EMBEDDED in this rootfs at
# /opt/diskos/mq_ui (present after a diskOS flash; needs no SD card), else (2) <SD>/diskos/mq_ui as
# a fallback source. EITHER source is copied ONLY after verifying it against the manifest baked into
# this rootfs (/etc/diskos_manifest): exact size, ELF32-LE, MIPS machine, and SHA-256. This stops a
# corrupt, wrong, or substituted UI from being copied in and run as root. Read-only SD mount.
#
# FAIL-CLOSED CONTRACT: the diskOS boot override in fiio_init.sh runs our UI ONLY when BOTH
# /usr/data/mq_ui AND the /usr/data/mq_player symlink exist; otherwise it falls back to the STOCK
# rootfs UI. So the load-bearing safety lever is the mq_player symlink: whenever we cannot PROVE
# /usr/data/mq_ui matches the manifest (missing/bad manifest, failed verify, failed repair), we
# remove that symlink (and move the binary aside) so the stock UI runs instead of an unverified
# binary. Every SD-touching op is time-bounded so a faulty card can delay but never hang boot.
#
# RESIDUAL LIMITS (documented, not shell-fixable): a process wedged in uninterruptible (D-state)
# kernel I/O cannot be killed by any signal, so a truly dead SD controller can still stall this
# script until the kernel gives up on the I/O; and if BusyBox lacks the `timeout` applet the
# bounds degrade to best-effort. Neither is reachable from userspace shell.
# EARLIEST fail-closed trap - the FIRST executable statement in the script (only comments precede
# it; no file operation runs before it). For every catchable TERMINATING signal it arms an inline
# handler that performs the load-bearing action (drop the mq_player symlink -> boot override
# disabled; move the binary aside) and exit 1, so even a signal during setup can't leave a
# pre-existing unverified override enabled for S98. Ordered most-likely-first (TERM/HUP/INT) because
# POSIX sh has no atomic multi-signal trap: the sub-microsecond gradual-arming window across the
# loop is an irreducible shell limitation, minimized by arming the boot-relevant signals first.
# Excludes SIGKILL/SIGSTOP (uncatchable) and the non-terminating/stop/ignore-default signals
# (TSTP/TTIN/TTOU/WINCH/URG/CHLD/CONT). The trailing `7` is SIGEMT: busybox ash rejects the NAME
# "EMT" but the deployment arch is MIPS where SIGEMT=7, so it's armed numerically (SIGSTKFLT is
# undefined on MIPS, so nothing further is needed). Per-signal arming (`2>/dev/null || true`) so a
# name an odd build rejects can't abort the set. Superseded below by the guarded trap once
# quarantine() exists.
SIGS="TERM HUP INT QUIT PWR ABRT ALRM PIPE USR1 USR2 XCPU XFSZ ILL TRAP BUS FPE SEGV SYS VTALRM PROF IO 7"
_qtrap='rm -rf /usr/data/mq_player 2>/dev/null; [ -e /usr/data/mq_ui ] && mv -f /usr/data/mq_ui /usr/data/mq_ui.rejected 2>/dev/null; exit 1'
# Arm the standard signals FIRST - this loop is the first executable statement (only the two var
# assignments above, which touch no files, precede it), so the boot-relevant signals (TERM/HUP/INT)
# are covered immediately.
for s in $SIGS; do trap "$_qtrap" "$s" 2>/dev/null || true; done
# THEN arm each real-time signal (SIGRTMIN..SIGRTMAX, 32..127 on MIPS Linux) AND record it in SIGS
# within the SAME iteration - so no batch list-build ever precedes arming. RT signals are catchable
# and default-terminate; nothing sends them to a boot init script, armed only for completeness.
# Per-signal arming skips any number the running kernel doesn't define (a 64-signal host arms
# 32..64; the MIPS device arms 32..127).
n=32; while [ "$n" -le 127 ]; do trap "$_qtrap" "$n" 2>/dev/null || true; SIGS="$SIGS $n"; n=$((n+1)); done
LOG=/usr/data/diskos_install.log
log() { echo "$(date 2>/dev/null || true) S97: $*" >> "$LOG" 2>/dev/null; }
# override_on: true iff the boot hook would launch our UI (it needs BOTH regular files present).
override_on() { [ -f /usr/data/mq_ui ] && [ -f /usr/data/mq_player ]; }
# quarantine: fail-closed, with a checked postcondition. Drop the mq_player path FIRST (that alone
# disables the override), move the binary aside, then PROVE the override is off; if it somehow
# isn't (e.g. rm/mv failed), remove the binary itself as a last resort and, if even that fails,
# log CRITICAL and return non-zero rather than silently claiming safety. Returns 0 iff the override
# is provably disabled.
quarantine() {
rm -rf /usr/data/mq_player 2>/dev/null # clear file/dir/symlink at the path
[ -e /usr/data/mq_ui ] && mv -f /usr/data/mq_ui /usr/data/mq_ui.rejected 2>/dev/null
if override_on; then
rm -f /usr/data/mq_ui 2>/dev/null # last resort: remove the override binary
if override_on; then
log "CRITICAL: could NOT disable diskOS override${1:+ ($1)} (fs unwritable?) -- unverified UI MAY run"
return 1
fi
fi
log "quarantined${1:+ ($1)} -> stock UI will run"
return 0
}
# Now that quarantine() exists, UPGRADE the early inline trap to the guarded one: while installed!=1
# a signal quarantines (with the checked postcondition + CRITICAL logging) and exits non-zero; once
# installed=1 it skips quarantine so a completed install is never torn down. A stray signal before
# install merely downgrades to the STOCK UI (safe), never up to running something unverified.
# disarm() clears it on success paths. SIGKILL/SIGSTOP + D-state I/O are non-trappable residuals;
# and if /usr/data is unwritable, quarantine cannot unlink the override AND this script cannot stop
# the SysV dispatcher reaching S98 - unrecoverable-from-shell, logged CRITICAL (a true boot
# fail-stop belongs in the rootfs boot hook, tracked separately, not this S-script).
disarm() { for s in $SIGS; do trap - "$s" 2>/dev/null || true; done; }
installed=0
for s in $SIGS; do trap '[ "$installed" = 1 ] || quarantine "signal"; exit 1' "$s" 2>/dev/null || true; done
# TO: run a command under a hard time bound. Prefer SIGKILL (-s KILL) so a stuck-but-killable op
# is force-terminated, not just SIGTERM'd. Falls back to running directly only if `timeout` is
# absent (logged once) - the one case we cannot bound from shell.
warned_to=0
TO() {
if command -v timeout >/dev/null 2>&1; then
timeout -s KILL 60 "$@"
else
[ "$warned_to" = 1 ] || { log "WARNING: no 'timeout' applet -> SD ops are UNBOUNDED this boot"; warned_to=1; }
"$@"
fi
}
# publish_symlink: make an already-verified /usr/data/mq_ui runnable (exec bit + mq_player link) and
# PROVE the symlink resolves to exactly /usr/data/mq_ui. Nukes whatever sits at the mq_player path
# first (a pre-existing dir/file/stale symlink would otherwise make `ln -sf` succeed without
# publishing the right target). Returns non-zero on any failure so the caller can quarantine.
publish_symlink() {
chmod +x /usr/data/mq_ui 2>/dev/null || return 1 # a 0644 hash-match would boot-select but not exec
rm -rf /usr/data/mq_player 2>/dev/null # remove any file/dir/symlink at the path
ln -sf /usr/data/mq_ui /usr/data/mq_player 2>/dev/null || return 1
[ "$(readlink /usr/data/mq_player 2>/dev/null)" = /usr/data/mq_ui ] || return 1 # prove exact target
return 0
}
MAN=/etc/diskos_manifest
# No/'malformed manifest = we cannot verify anything -> fail closed (quarantine any existing override).
[ -f "$MAN" ] || { quarantine "no manifest" || exit 1; disarm; exit 0; }
MSHA=$(grep '^SHA256=' "$MAN" | cut -d= -f2)
MSIZE=$(grep '^SIZE=' "$MAN" | cut -d= -f2)
[ -n "$MSHA" ] && [ -n "$MSIZE" ] || { quarantine "malformed manifest" || exit 1; disarm; exit 0; }
# verify_ui <file>: 0 only if it exactly matches the manifest (size, ELF32-LE, MIPS, sha256).
verify_ui() {
f="$1"; [ -f "$f" ] || return 1
sz=$(stat -c%s "$f" 2>/dev/null); [ -n "$sz" ] || sz=$(wc -c < "$f" 2>/dev/null | tr -d ' ')
[ "$sz" = "$MSIZE" ] || { log "verify $f: size $sz != $MSIZE"; return 1; }
[ "$(od -An -tx1 -N4 "$f" | tr -d ' ')" = "7f454c46" ] || { log "verify $f: not ELF"; return 1; }
[ "$(od -An -tx1 -j4 -N2 "$f" | tr -d ' ')" = "0101" ] || { log "verify $f: not ELF32-LE"; return 1; } # EI_CLASS=32,EI_DATA=LE
[ "$(od -An -tx1 -j18 -N2 "$f" | tr -d ' ')" = "0800" ] || { log "verify $f: not MIPS"; return 1; }
[ "$(sha256sum "$f" | cut -d' ' -f1)" = "$MSHA" ] || { log "verify $f: sha256 mismatch"; return 1; }
return 0
}
rm -f /usr/data/.mq_ui.tmp 2>/dev/null # never trust a leftover temp from a prior interrupted run
# FAST PATH: an already-installed matching UI -> just repair exec bit + symlink and boot.
if verify_ui /usr/data/mq_ui; then
if publish_symlink; then
installed=1; disarm # a completed install: disarm before exit so no late-signal quarantine
log "already installed + verified -> repaired +x/symlink, booting diskOS"
exit 0
fi
log "already-installed repair (chmod/ln) failed -> quarantining"
quarantine "repair failed" || exit 1 # can't guarantee it launches -> fall back to stock
disarm; exit 0
fi
# Reaching here means /usr/data/mq_ui is absent or does NOT match the manifest. Pre-emptively
# QUARANTINE any existing override NOW - BEFORE the SD window - so an interrupted copy can never
# leave the unverified binary enabled for S98 to launch. A successful SD install below republishes
# a verified one. (The whole-run trap above already covers signals; this closes the window
# deterministically even absent a signal.)
if [ -e /usr/data/mq_ui ] || [ -e /usr/data/mq_player ]; then
quarantine "unverified at boot" || exit 1
fi
# INSTALL PATH - SOURCE PRECEDENCE (deterministic, NOT newest-wins): try the copy EMBEDDED in this
# rootfs at /opt/diskos/mq_ui FIRST (present after a diskOS flash, needs no SD card); if it is absent
# OR its local copy fails manifest verification, fall through to <SD>/diskos/mq_ui as a recovery
# source. The embedded source only "wins" (copied=1, SD skipped) when its temp passes verify_ui - so
# a valid same-hash SD copy CAN rescue a corrupted embedded copy. The SD is a fallback SOURCE, not an
# override (no version/newer-wins). The winning temp is re-verified + published below.
copied=0; src=
EMBED=/opt/diskos/mq_ui
if [ -f "$EMBED" ]; then
# Embedded copy lives in the read-only rootfs we just flashed. TO-bound the cp for consistency
# (a NAND read fault shouldn't stall boot), and verify_ui the temp IN-BRANCH: only a verified
# embedded copy suppresses the SD fallback.
if TO cp "$EMBED" /usr/data/.mq_ui.tmp 2>/dev/null && verify_ui /usr/data/.mq_ui.tmp; then
copied=1; src=embedded; log "staged verified UI from embedded rootfs copy ($EMBED)"
else
rm -f /usr/data/.mq_ui.tmp 2>/dev/null
log "embedded UI absent or failed verify -> trying SD fallback"
fi
fi
# SD FALLBACK: only when the rootfs carried no VERIFIED UI. Mount the SD read-only, copy
# <SD>/diskos/mq_ui to the LOCAL temp, unmount; mount/cp/umount are all TO-bounded; no verification
# ever runs against the (possibly faulty) card (the publish block below verifies the local copy).
if [ "$copied" != 1 ]; then
MP=/tmp/diskos_sd; mkdir -p "$MP"; mounted=0
for dev in /dev/mmcblk0p1 /dev/mmcblk1p1 /dev/mmcblk0 /dev/mmcblk1; do
[ -b "$dev" ] || continue
for fs in exfat vfat; do
TO mount -t $fs -o ro "$dev" "$MP" 2>/dev/null && { mounted=1; break; }
done
[ "$mounted" = 1 ] && break
done
if [ "$mounted" = 1 ]; then
# No pre-stat of the SD path (that could hang) - let the bounded cp fail fast if it's absent.
if TO cp "$MP/diskos/mq_ui" /usr/data/.mq_ui.tmp 2>/dev/null; then
copied=1; src=SD
else
log "no readable <SD>/diskos/mq_ui (or copy timed out) -> nothing to install"
fi
if TO umount "$MP" 2>/dev/null || TO umount -l "$MP" 2>/dev/null; then :; else
log "WARNING: SD would not unmount (card may stay mounted; publish is unaffected - it uses the local copy)"
fi
else
log "no SD mounted -> nothing to install this boot"
fi
fi
if [ "$copied" = 1 ]; then
# Verify the LOCAL copy, then publish atomically. installed=1 is gated on the CORRECTNESS steps
# only (verify -> chmod -> mv -> publish_symlink[proves exact target] -> re-verify); publishing
# is independent of whether the SD unmounted, since it works entirely on the local copy.
# Durability rides on /usr/data being sync-mounted ubifs; the explicit sync is TO-bounded and
# deliberately NOT part of the success gate.
if verify_ui /usr/data/.mq_ui.tmp \
&& chmod +x /usr/data/.mq_ui.tmp \
&& mv -f /usr/data/.mq_ui.tmp /usr/data/mq_ui \
&& publish_symlink \
&& verify_ui /usr/data/mq_ui; then
installed=1; log "installed verified mq_ui from ${src:-?} (size $MSIZE)"
TO sync 2>/dev/null || log "post-install sync slow/timed-out (ubifs is sync-mounted; already durable)"
else
log "SD copy failed verification/publish -> not installed"
fi
fi
rm -f /usr/data/.mq_ui.tmp 2>/dev/null # always clean the temp, incl. a timed-out/partial copy
# FINALIZE (fail-closed): if we did not publish this boot, make sure only a manifest-verified UI
# can run. A verified-but-unpublished binary gets its exec bit + symlink repaired; anything that
# does NOT verify is quarantined so the stock UI runs.
if [ "$installed" != 1 ]; then
if verify_ui /usr/data/mq_ui; then
publish_symlink || quarantine "finalize repair failed" || exit 1
elif [ -e /usr/data/mq_ui ] || [ -e /usr/data/mq_player ]; then
quarantine "unverified override" || exit 1
fi
fi
disarm # clean end: fail-closed state is settled, disarm so no late signal quarantines it
exit 0
+88
View File
@@ -0,0 +1,88 @@
#!/bin/sh
# diskOS dev/recovery: USB CDC-ACM serial console on the device USB-C port.
# Host enumerates 0525:a4a7 -> /dev/ttyACM0 -> root shell (raw, no login).
#
# Robust against the failure that killed the previous attempt: the stock
# serial_config.sh binds the UDC exactly once and never retries, so a UDC that
# registers (or a host that plugs in) a moment later was missed. This runs a
# persistent supervisor: (re)bind whenever a UDC is present, keep a shell alive
# on ttyGS0. All steps best-effort + logged to a persistent (rw) partition.
LOG=/usr/data/fiio/usbserial.log
G=/sys/kernel/config/usb_gadget/serial_demo
log() { echo "$(date 2>/dev/null) $*" >> "$LOG" 2>/dev/null; }
build_gadget() {
[ -d /sys/kernel/config/usb_gadget ] || mount -t configfs none /sys/kernel/config 2>/dev/null
[ -d "$G" ] && return 0
mkdir -p "$G" || return 1
echo 0x0525 > "$G/idVendor"
echo 0xa4a7 > "$G/idProduct"
echo 0x0200 > "$G/bcdUSB"
echo 0x2400 > "$G/bcdDevice"
echo 0x02 > "$G/bDeviceClass"
echo 0x00 > "$G/bDeviceSubClass"
echo 0x00 > "$G/bDeviceProtocol"
mkdir -p "$G/strings/0x409"
echo "INGENIC" > "$G/strings/0x409/manufacturer"
echo "diskOS Serial" > "$G/strings/0x409/product"
echo "diskos-serial" > "$G/strings/0x409/serialnumber"
mkdir -p "$G/configs/c.1/strings/0x409"
echo 120 > "$G/configs/c.1/MaxPower"
echo 0x80 > "$G/configs/c.1/bmAttributes"
echo "serial" > "$G/configs/c.1/strings/0x409/configuration"
mkdir -p "$G/functions/acm.0"
ln -sf "$G/functions/acm.0" "$G/configs/c.1/" 2>/dev/null
log "gadget built"
}
# Best-effort nudge into peripheral mode. dr_mode=otg already auto-switches to
# device when plugged into a host (CC/pin), so this is only a safety net. Only
# the unambiguous "device" token is written -- never anything that could select
# host mode and break enumeration.
force_device_mode() {
for f in /sys/devices/platform/*otg*/dwc2_mode \
/sys/devices/platform/*otg*/dwc2_force_dr_mode \
/sys/devices/platform/*/13500000.otg/dwc2_mode; do
[ -e "$f" ] || continue
echo device > "$f" 2>/dev/null && log "wrote device-mode to $f"
done
}
bound() { [ -n "$(cat "$G/UDC" 2>/dev/null)" ]; }
bind_when_ready() {
udc=$(ls /sys/class/udc/ 2>/dev/null | head -n1)
[ -n "$udc" ] || return 1
echo "$udc" > "$G/UDC" 2>/dev/null && { log "bound UDC=$udc"; return 0; }
return 1
}
# Delegate the ttyGS0 shell to diskos-debug (the SINGLE owner - fuser-guarded + self-respawning).
# Two independent shell respawners on one tty is the classic "answers once then goes silent" wedge,
# so this script no longer binds its own shell; it only builds the gadget and asks diskos-debug to
# attach the one shell. Idempotent: serial-on is a no-op if a shell already owns ttyGS0.
DEBUG=/usr/project/diskos-debug.sh
ensure_shell() {
[ -c /dev/ttyGS0 ] || return 0
[ -x "$DEBUG" ] && "$DEBUG" serial-on >/dev/null 2>&1
}
main() {
: > "$LOG" 2>/dev/null
log "S99usbserial start"
build_gadget
force_device_mode
while true; do
bound || bind_when_ready
ensure_shell
sleep 2
done
}
case "$1" in
start) main & ;;
*) exit 1 ;;
esac
exit 0
+165
View File
@@ -0,0 +1,165 @@
#!/bin/sh
# diskOS debug access. Two independent channels, both OFF unless explicitly enabled:
# - SSH (dropbear over WiFi) with a RANDOM per-enable password. We NEVER use or expose the
# stock root password: the caller (mq_ui) passes a fresh sha512 crypt hash, which we place in a
# private shadow file bind-mounted over /etc/shadow (the on-disk stock /etc/shadow is untouched).
# - SERIAL (a root shell on the USB gadget /dev/ttyGS0). Local USB only; no network exposure.
# State + keys live under /usr/data/sshd (persists across rootfs flashes). Idempotent; every op is
# best-effort so a missing tool or busy resource never wedges the caller.
#
# Usage:
# diskos-debug.sh ssh-on <root_sha512_hash> # start dropbear with this password hash
# diskos-debug.sh ssh-off # stop dropbear + drop the shadow overlay
# diskos-debug.sh serial-on # bind ONE root shell to /dev/ttyGS0
# diskos-debug.sh serial-off # stop the serial shell
# diskos-debug.sh status # print: SSH=on/off SERIAL=on/off
SELF=/usr/data/sshd
DB="$SELF/dropbearmulti"
KEYS="$SELF/keys"
SHADOW="$SELF/shadow" # our private shadow, bind-mounted over /etc/shadow while SSH is on
SERIALPID="$SELF/serial.pid" # pid of the single ttyGS0 shell supervisor
BUNDLED=/usr/project/dropbearmulti # read-only rootfs copy shipped in the image
# FAIL-CLOSED test for the private shadow overlay. Returns 0 (present) if it is bind-mounted over
# /etc/shadow OR if we cannot tell (/proc/mounts unreadable); returns 1 (absent) ONLY when a readable
# /proc/mounts definitively shows no such mount. `grep` exit codes: 0=match, 1=no match, >=2=error;
# we must treat "no match" as absent but any error as still-present, so we never report a clean state
# on uncertainty.
overlay_present() {
grep -q ' /etc/shadow ' /proc/mounts 2>/dev/null
_g=$?
[ "$_g" -eq 1 ] && return 1 # readable, definitively no overlay
return 0 # matched (0) or read error (>=2) -> present/unknown, fail closed
}
ensure_db() {
mkdir -p "$KEYS" 2>/dev/null
# Provision/refresh the dropbear binary from the shipped rootfs copy (survives flashes on /usr/data).
if [ ! -x "$DB" ] && [ -f "$BUNDLED" ]; then cp "$BUNDLED" "$DB" 2>/dev/null; chmod +x "$DB" 2>/dev/null; fi
[ -x "$DB" ] || return 1
[ -s "$KEYS/ed25519_host_key" ] || "$DB" dropbearkey -t ed25519 -f "$KEYS/ed25519_host_key" >/dev/null 2>&1
[ -s "$KEYS/rsa_host_key" ] || "$DB" dropbearkey -t rsa -s 2048 -f "$KEYS/rsa_host_key" >/dev/null 2>&1
return 0
}
# FAIL-CLOSED: SSH starts only once the private-password overlay is PROVEN active. Any failure (no
# hash, stuck overlay, empty/garbled shadow, bind failure, dropbear not up) returns non-zero WITHOUT
# leaving dropbear authenticating against the stock /etc/shadow.
ssh_on() {
hash="$1"
[ -n "$hash" ] || { echo "err: no hash"; return 1; }
ensure_db || { echo "err: no dropbear binary"; return 1; }
# STOP any already-running dropbear BEFORE touching the overlay. Otherwise, in the window between
# unmounting the old overlay and binding the new one, a running daemon would authenticate against
# the STOCK /etc/shadow (and any later failure would leave it there). No daemon runs during the swap.
for p in $(pgrep -f "dropbearmulti dropbear" 2>/dev/null); do kill "$p" 2>/dev/null; done
i=0; while pgrep -f "dropbearmulti dropbear" >/dev/null 2>&1 && [ "$i" -lt 5 ]; do sleep 1; i=$((i+1)); done
for p in $(pgrep -f "dropbearmulti dropbear" 2>/dev/null); do kill -9 "$p" 2>/dev/null; done
pgrep -f "dropbearmulti dropbear" >/dev/null 2>&1 && { echo "err: could not stop existing dropbear"; return 1; }
# Drop any existing overlay and CONFIRM it is gone - reading /etc/shadow while it still aliases
# $SHADOW would let `>` truncate it to empty. If it will not unmount, refuse.
if overlay_present; then
umount /etc/shadow 2>/dev/null
overlay_present && { echo "err: shadow overlay stuck"; return 1; }
fi
[ -f /etc/shadow ] || { echo "err: no /etc/shadow"; return 1; }
# Build the private shadow (root line = our random hash) and PROVE it holds that hash before
# trusting it - never bind an empty or malformed shadow.
awk -v h="$hash" -F: 'BEGIN{OFS=":"} $1=="root"{$2=h} {print}' /etc/shadow > "$SHADOW" 2>/dev/null \
|| { echo "err: shadow gen failed"; return 1; }
chmod 600 "$SHADOW" 2>/dev/null
awk -F: -v h="$hash" '$1=="root" && $2==h{ok=1} END{exit ok?0:1}' "$SHADOW" 2>/dev/null \
|| { echo "err: shadow bad"; rm -f "$SHADOW"; return 1; }
mount -o bind "$SHADOW" /etc/shadow 2>/dev/null || mount --bind "$SHADOW" /etc/shadow 2>/dev/null
# CONFIRM the overlay is live BEFORE starting SSH; if the bind failed, dropbear would authenticate
# against the STOCK /etc/shadow (stock root password reachable over the network) - refuse.
grep -q ' /etc/shadow ' /proc/mounts 2>/dev/null || { echo "err: overlay failed, refusing SSH"; return 1; }
pgrep -f "dropbearmulti dropbear" >/dev/null 2>&1 || \
"$DB" dropbear -p 22 -r "$KEYS/ed25519_host_key" -r "$KEYS/rsa_host_key" >/dev/null 2>&1
if ! pgrep -f "dropbearmulti dropbear" >/dev/null 2>&1; then
# dropbear failed to start: tear the overlay back down and CONFIRM it is gone (retry once).
# Report which state we ended in so the caller never assumes a clean OFF while it is still mounted.
umount /etc/shadow 2>/dev/null
overlay_present && { sleep 1; umount /etc/shadow 2>/dev/null; }
if overlay_present; then
echo "err: dropbear did not start AND overlay stuck"; return 2
fi
echo "err: dropbear did not start"; return 1
fi
echo on
}
# FAIL-CLOSED: kill dropbear and CONFIRM it is gone BEFORE dropping the overlay (a surviving process
# would otherwise fall back to the stock /etc/shadow once the bind is removed). Report the REAL state.
ssh_off() {
for p in $(pgrep -f "dropbearmulti dropbear" 2>/dev/null); do kill "$p" 2>/dev/null; done
i=0; while pgrep -f "dropbearmulti dropbear" >/dev/null 2>&1 && [ "$i" -lt 4 ]; do sleep 1; i=$((i+1)); done
for p in $(pgrep -f "dropbearmulti dropbear" 2>/dev/null); do kill -9 "$p" 2>/dev/null; done
# brief settle, then drop the overlay. Whole path stays under ~6s so the UI's bounded call
# (12s, see debug_ui.c disable_dbg) always lets us finish the umount before it can kill us -
# otherwise the UI could report OFF while the overlay is still mounted.
i=0; while pgrep -f "dropbearmulti dropbear" >/dev/null 2>&1 && [ "$i" -lt 2 ]; do sleep 1; i=$((i+1)); done
# NEVER drop the overlay while a daemon might still be alive: if a process somehow survived SIGKILL,
# unmounting would drop it back onto the STOCK /etc/shadow. Keep the overlay (so it stays bound to
# OUR shadow) and fail instead. Only umount once dropbear is CONFIRMED gone.
if pgrep -f "dropbearmulti dropbear" >/dev/null 2>&1; then
echo "err: dropbear still running - overlay kept, refusing to expose stock shadow"; return 1
fi
overlay_present && umount /etc/shadow 2>/dev/null
if overlay_present; then
echo "err: overlay stuck"; return 1
fi
echo off
}
# True only if $SERIALPID names a LIVE process that is actually OUR supervisor. A bare `kill -0` would
# be fooled by PID reuse (some unrelated process now holding that number) - so we also confirm the
# process's cmdline still contains the ttyGS0 supervisor marker before trusting/signalling it.
serial_alive() {
[ -f "$SERIALPID" ] || return 1
_sp=$(cat "$SERIALPID" 2>/dev/null); [ -n "$_sp" ] || return 1
kill -0 "$_sp" 2>/dev/null || return 1
tr '\0' ' ' < "/proc/$_sp/cmdline" 2>/dev/null | grep -q 'ttyGS0'
}
serial_on() {
[ -c /dev/ttyGS0 ] || { echo "no ttyGS0"; return 1; }
# Exactly ONE supervisor owns ttyGS0. A plain `fuser` guard is NOT enough: between shell respawns
# the supervisor sleeps 1s without holding the tty, so a repeated serial-on (e.g. S99's loop) would
# see no owner and start a SECOND supervisor = the two-shell wedge. Guard on the SUPERVISOR pid
# (which stays alive across that gap, validated as really ours) via a pidfile instead.
if serial_alive; then echo on; return 0; fi
rm -f "$SERIALPID" 2>/dev/null # stale/reused pid - clear it so a real supervisor can start
setsid sh -c 'echo $$ > '"$SERIALPID"'; while true; do /bin/sh </dev/ttyGS0 >/dev/ttyGS0 2>&1; sleep 1; done' </dev/null >/dev/null 2>&1 &
# Confirm the supervisor actually came up (setsid/fork/pidfile-write can all fail) before claiming
# success - otherwise the caller would show "serial on" with no shell behind it. Bounded to ~2s
# (portable whole-second sleep) so it stays well under the UI's serial-on timeout.
serial_alive || sleep 1
serial_alive || sleep 1
if serial_alive; then echo on; return 0; fi
echo "err: serial supervisor did not start"; return 1
}
serial_off() {
# Only signal the pidfile's process if it is verifiably OUR supervisor (never a reused PID).
if serial_alive; then kill "$(cat "$SERIALPID" 2>/dev/null)" 2>/dev/null; fi
rm -f "$SERIALPID" 2>/dev/null
fuser -k /dev/ttyGS0 2>/dev/null
echo off
}
status() {
s=off; pgrep -f "dropbearmulti dropbear" >/dev/null 2>&1 && s=on
r=off; serial_alive && r=on
echo "SSH=$s SERIAL=$r"
}
case "$1" in
ssh-on) ssh_on "$2" ;;
ssh-off) ssh_off ;;
serial-on) serial_on ;;
serial-off) serial_off ;;
status) status ;;
*) echo "usage: $0 {ssh-on <hash>|ssh-off|serial-on|serial-off|status}"; exit 1 ;;
esac
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.