Files
b0hemia e0bc4785e9 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.
2026-08-26 15:26:14 +10:00

166 lines
9.5 KiB
Bash
Executable File

#!/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