utils/vmbox/lib/ssh.sh
Krzysztof kuhy Rudnicki 8bb4d3a60d vmbox: bound every wait, so an unbootable guest fails instead of hanging
Testing boot_recovery produced a guest that cannot boot: boot-repair read
/boot/vmlinuz-linux and initramfs-linux.img on this BIOS/GRUB guest as
"orphaned kernel files shadowing the ESP" and deleted them, leaving the
sandbox at a GRUB prompt. That is a legitimate sandbox outcome -- destroying
the guest is what the tool is for -- but vmbox handled it by hanging.

Two independent unbounded waits, both fixed:

- vm_wait_ssh counted only its `sleep 2` calls, ignoring the up-to-5s
  ConnectTimeout each failed probe spends inside ssh. A nominal 150s wait ran
  past 400s (measured, then cut short by the harness). It now works to a wall
  clock deadline: the same guest is now reported unreachable in 156s.

- _run_recover_rc reboots the stopped guest to read the recorded exit code,
  and fell through to an unbounded serial console read: 180s for a prompt
  that never comes plus 300s for a command that never runs, all AFTER the
  verdict had already been printed. Recovery is best-effort by definition, so
  both bounds are now short (60s/30s) and overridable.

serial_exec's timeouts become env-overridable to make that possible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
2026-08-22 22:31:39 +02:00

94 lines
3.3 KiB
Bash

#!/bin/bash
# ============================================================================
# vmbox: ssh access to a sandbox.
#
# Guest host keys are deliberately NOT verified and NOT recorded: a sandbox is
# reset constantly, so a real known_hosts entry would collide on every reset.
# Isolation comes from the VM boundary, not from ssh key continuity.
# ============================================================================
# shellcheck source=common.sh
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
vm_ssh_port() {
local port
port="$(meta_get "$1" ssh_port)" || die "sandbox '$1' has no ssh port in meta"
printf '%s' "$port"
}
_ssh_base_args() {
local port="$1"
printf '%s\0' \
-i "$VMBOX_SSH_KEY" \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o GlobalKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
-o ConnectTimeout=5 \
-o ServerAliveInterval=5 \
-p "$port"
}
# Run a command in the guest. Returns the guest's exit code, EXCEPT when the
# connection dies -- ssh reports 255 for a dropped link, a killed sshd and a
# successful poweroff alike, which is exactly why the verdict logic never
# trusts this return value on its own.
vm_ssh_exec() {
local name="$1"; shift
local port; port="$(vm_ssh_port "$name")"
local -a args=()
mapfile -d '' -t args < <(_ssh_base_args "$port")
ssh "${args[@]}" "${VMBOX_GUEST_USER}@127.0.0.1" "$@"
}
vm_ssh_interactive() {
local name
name="$(validate_vm_name "${1:-}")"; shift || true
require_vm "$name"
if ! vm_is_running "$name"; then
log "Sandbox '$name' is not running; starting it"
source "$VMBOX_LIB_DIR/launch.sh"
launch_vm "$name"
vm_wait_ssh "$name" 120 || die "sandbox '$name' did not become reachable"
fi
local port; port="$(vm_ssh_port "$name")"
local -a args=()
mapfile -d '' -t args < <(_ssh_base_args "$port")
# -t forces a pty so interactive tools (and sudo prompts) behave.
ssh -t "${args[@]}" "${VMBOX_GUEST_USER}@127.0.0.1" "$@"
}
vm_scp_to() {
local name="$1" src="$2" dest="$3" port
port="$(vm_ssh_port "$name")"
scp -i "$VMBOX_SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -P "$port" -r "$src" "${VMBOX_GUEST_USER}@127.0.0.1:$dest"
}
# Poll until the guest answers. ssh is its own port prober, so this needs no
# nc/socat (neither is installed on this host).
vm_wait_ssh() {
local name="$1" timeout="${2:-120}" deadline start now
# Deadline is WALL CLOCK, not a count of sleeps. Each failed probe also
# burns up to ConnectTimeout seconds inside ssh itself, so counting only
# the sleeps under-measures badly: a guest that never boots (no kernel,
# stuck in GRUB) made a nominal 150s wait run for well over 10 minutes,
# which reads as a hung tool rather than an unreachable sandbox.
printf -v start '%(%s)T' -1
deadline=$(( start + timeout ))
while :; do
printf -v now '%(%s)T' -1
(( now < deadline )) || return 1
if vm_is_running "$name" && vm_ssh_exec "$name" -o BatchMode=yes true 2>/dev/null; then
return 0
fi
# A qemu that has exited means the guest will never answer.
if ! vm_is_running "$name"; then
(( now - start > 3 )) && return 1
fi
sleep 2
done
}