Skip to content
Merged
46 changes: 43 additions & 3 deletions docker/qemu-aarch64/run-arm64-boot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BREENIX_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"

# #826/#834/R181: this script's qemu-system-aarch64 boot runs behind the
# host-wide lock in docker/qemu/lib/qemu-host-lock.sh. This script is
# Docker-wrapped -- see docker/qemu/run-aarch64-test.sh's identical comment
# for why the host-side lock still applies (the `docker run` CLI blocks on
# the host with the qemu-system-aarch64 token in its own argv). #834
# extends this lock's coverage from docker/qemu/*.sh (its original
# #826/R181 scope, which this sibling directory falls outside of) to the
# rest of the tree that launches qemu-system-aarch64.
# shellcheck source=../qemu/lib/qemu-host-lock.sh
source "$SCRIPT_DIR/../qemu/lib/qemu-host-lock.sh"

# #834 fix-round F3 (2026-09-05): OUTPUT_DIR is a HOST path this script
# rm -rf's, mkdir's, and bind-mounts into the container, so it collides with
# any other concurrent invocation the same way #825 already reports for
# docker/qemu/run-aarch64-test.sh's OUTPUT_DIR (see that file's own #825
# comment for the identical BREENIX_GATE_TMP shape). Defaulting to /tmp
# keeps a caller that leaves it unset byte-identical; a concurrent-lane
# launcher sets this to a per-worktree directory instead.
BREENIX_GATE_TMP="${BREENIX_GATE_TMP:-/tmp}"
case "$BREENIX_GATE_TMP" in
/*) ;;
*) echo "FAIL: BREENIX_GATE_TMP must be an absolute path, got: $BREENIX_GATE_TMP"; exit 1 ;;
esac

# Find the ARM64 kernel binary
KERNEL_BIN="$BREENIX_ROOT/target/aarch64-breenix-kernel/release/kernel-aarch64"
if [ ! -f "$KERNEL_BIN" ]; then
Expand All @@ -19,7 +43,7 @@ echo "Running ARM64 boot test in Docker..."
echo "Kernel: $KERNEL_BIN"

# Create output directory
OUTPUT_DIR="/tmp/breenix_arm64_boot"
OUTPUT_DIR="$BREENIX_GATE_TMP/breenix_arm64_boot"
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"

Expand All @@ -32,7 +56,17 @@ docker build -q -t breenix-qemu-aarch64 "$SCRIPT_DIR" > /dev/null
# - 1 CPU
# - PL011 UART for serial output
# - No graphics
# #834 fix-round F2 (2026-09-05): CONTAINER_NAME is unique to this
# invocation (this script's own PID), so the cleanup `docker kill` below
# can target the exact container this run started instead of matching by
# ancestor image -- an ancestor-image filter can kill a DIFFERENT running
# container from that image, including one a concurrent invocation of this
# same script (or of another script sharing the image) legitimately owns.
CONTAINER_NAME="breenix-arm64-boot-$$"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
qemu_host_lock_acquire
docker run --rm \
--name "$CONTAINER_NAME" \
-v "$KERNEL_BIN:/breenix/kernel.elf:ro" \
-v "$OUTPUT_DIR:/output" \
breenix-qemu-aarch64 \
Expand All @@ -48,6 +82,11 @@ docker run --rm \
-no-reboot \
&
QEMU_PID=$!
# F2: registers the docker run client with the lock's own EXIT trap (see
# docker/qemu/lib/qemu-host-lock.sh) so a SIGTERM/SIGINT delivered to just
# this process during the poll below still stops the container instead of
# orphaning it with the lock free.
qemu_host_lock_track_pid "$QEMU_PID"

# Wait for output or timeout
echo "Waiting for kernel output (30s timeout)..."
Expand All @@ -64,8 +103,9 @@ for i in $(seq 1 30); do
sleep 1
done

# Cleanup
docker kill $(docker ps -q --filter ancestor=breenix-qemu-aarch64) 2>/dev/null || true
# Cleanup: targets this invocation's own container by name (see
# CONTAINER_NAME above), not the whole image's running containers.
docker kill "$CONTAINER_NAME" 2>/dev/null || true

# Check results
echo ""
Expand Down

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BREENIX_ROOT="$SCRIPT_DIR"
cd "$BREENIX_ROOT"

# #826/#834/R181: this script's native (non-Parallels, non-VMware)
# qemu-system-aarch64 boot runs behind the host-wide lock in
# docker/qemu/lib/qemu-host-lock.sh -- sourced unconditionally since arch is
# runtime-selected, but only the arm64 leg below actually calls
# qemu_host_lock_acquire (the lock serializes qemu-system-aarch64 boots
# specifically; the x86_64 leg's qemu-system-x86_64 is outside its scope).
# The Parallels and VMware code paths above this point boot a VM, not a
# native qemu-system-aarch64 process, and exit before reaching here. #834
# extends this lock's coverage from docker/qemu/*.sh (its original
# #826/R181 scope) to run.sh as well.
# shellcheck source=docker/qemu/lib/qemu-host-lock.sh
source "$BREENIX_ROOT/docker/qemu/lib/qemu-host-lock.sh"

# Defaults: ARM64 with graphics
ARCH="arm64"
HEADLESS=false
Expand Down Expand Up @@ -1062,6 +1075,13 @@ if [ "$ARCH" = "arm64" ]; then
# MMIO addresses in reverse command-line order. The kernel discovers devices
# by scanning MMIO addresses low-to-high, so the LAST device here gets the
# lowest address and becomes device 0 (the system/root disk).
#
# #834: this is an interactive display session (Ctrl+C stops QEMU below),
# but it still takes the host-wide lock -- not exempt just because a
# human, not a gate, is driving it. qemu_host_lock_acquire prints its own
# one-line notice (host count, then a wait message if contended) before
# blocking.
qemu_host_lock_acquire
qemu-system-aarch64 \
-M virt,gic-version=3 -cpu max -smp 4 \
-m 512M \
Expand Down Expand Up @@ -1117,6 +1137,13 @@ else
fi

QEMU_PID=$!
if [ "$ARCH" = "arm64" ]; then
# F2: registers QEMU with the lock's own EXIT trap (see
# docker/qemu/lib/qemu-host-lock.sh) so a SIGTERM/SIGINT delivered to
# just this script's own PID still kills QEMU instead of orphaning it
# with the lock free.
qemu_host_lock_track_pid "$QEMU_PID"
fi

echo "Paste: echo 'code' | ./scripts/paste.sh"
echo "Monitor: tcp://127.0.0.1:4444"
Expand Down
33 changes: 31 additions & 2 deletions scripts/run-aarch64-userspace.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BREENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$BREENIX_ROOT"

# #826/#834/R181: this script's qemu-system-aarch64 boot runs behind the
# host-wide lock in docker/qemu/lib/qemu-host-lock.sh -- #834 extends that
# lock's coverage from docker/qemu/*.sh (its original #826/R181 scope) to
# scripts/ as well.
# shellcheck source=../docker/qemu/lib/qemu-host-lock.sh
source "$BREENIX_ROOT/docker/qemu/lib/qemu-host-lock.sh"

# Build ARM64 kernel
KERNEL="$BREENIX_ROOT/target/aarch64-breenix-kernel/release/kernel-aarch64"
if [ ! -f "$KERNEL" ]; then
Expand Down Expand Up @@ -48,7 +55,25 @@ case "$(uname)" in
*) DISPLAY_OPT="-display sdl" ;;
esac

exec qemu-system-aarch64 \
# #834: this is an interactive display session (Ctrl-A X exits QEMU), but
# it still takes the host-wide lock -- not exempt just because a human, not
# a gate, is driving it. `exec` is dropped because replacing this shell's
# own process image would discard the qemu_host_lock_acquire EXIT trap
# before it could release the lock. A plain foreground launch (no `&`) is
# not a safe substitute for `exec`, though: a SIGTERM/SIGINT delivered to
# just this script's own PID does not propagate to a foreground child on
# its own, so the EXIT trap would run and release the lock while QEMU kept
# running, orphaned and untracked -- reproducing the exact unserialized
# double-boot contention this lock exists to prevent (#834 fix-round F1,
# 2026-09-05). QEMU is backgrounded instead, with its PID handed to
# qemu_host_lock_track_pid so the lock's own EXIT trap kills it before
# releasing the lock on that path, then `wait`ed on so this script still
# blocks in the foreground exactly as before. `0<&0` is required on the
# backgrounded launch: bash redirects a backgrounded command's stdin from
# /dev/null unless the command carries its own explicit stdin redirection,
# and this session's serial monitor needs this script's own stdin attached.
qemu_host_lock_acquire
qemu-system-aarch64 \
-M virt \
-cpu cortex-a72 \
-m 512M \
Expand All @@ -59,4 +84,8 @@ exec qemu-system-aarch64 \
-blockdev driver=file,node-name=testfile,filename="$TEST_DISK" \
-blockdev driver=raw,node-name=testdisk,file=testfile \
-device virtio-keyboard-device \
-kernel "$KERNEL"
-kernel "$KERNEL" \
0<&0 &
QEMU_PID=$!
qemu_host_lock_track_pid "$QEMU_PID"
wait "$QEMU_PID"
42 changes: 39 additions & 3 deletions scripts/run-arm64-boot-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,29 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BREENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$BREENIX_ROOT"

# #826/#834/R181: this script's qemu-system-aarch64 boot runs behind the
# host-wide lock in docker/qemu/lib/qemu-host-lock.sh -- #834 extends that
# lock's coverage from docker/qemu/*.sh (its original #826/R181 scope) to
# scripts/ as well.
# shellcheck source=../docker/qemu/lib/qemu-host-lock.sh
source "$BREENIX_ROOT/docker/qemu/lib/qemu-host-lock.sh"

# #834 fix-round F3 (2026-09-05): SERIAL_OUTPUT is a HOST path this script
# rm -f's and then points QEMU's `-serial file:` at, so it collides with any
# other concurrent invocation the same way #825 already reports for
# docker/qemu/run-aarch64-test.sh's OUTPUT_DIR (see that file's own #825
# comment for the identical BREENIX_GATE_TMP shape). Defaulting to /tmp
# keeps a caller that leaves it unset byte-identical; a concurrent-lane
# launcher sets this to a per-worktree directory instead.
BREENIX_GATE_TMP="${BREENIX_GATE_TMP:-/tmp}"
case "$BREENIX_GATE_TMP" in
/*) ;;
*) echo "FAIL: BREENIX_GATE_TMP must be an absolute path, got: $BREENIX_GATE_TMP"; exit 1 ;;
esac

# Configuration
KERNEL_PATH="target/aarch64-breenix-kernel/release/kernel-aarch64"
SERIAL_OUTPUT="/tmp/arm64_boot_test_output.txt"
SERIAL_OUTPUT="$BREENIX_GATE_TMP/arm64_boot_test_output.txt"
TIMEOUT_SECS=30
TEST_MODE="${1:-full}"

Expand Down Expand Up @@ -65,6 +85,15 @@ fi
echo "Kernel built: $KERNEL_PATH"
echo ""

# #834 fix-round F2 (2026-09-05): qemu_host_lock_acquire moves here, before
# the cleanup pkill below, so the pattern-based kill cannot hit another
# lock-cooperating script's own in-progress, lock-protected boot -- by the
# time this script holds the lock, any such script must already have
# released it (or this script would still be blocked waiting to acquire).
# A stray process the pkill still reaches after the lock is held is, by
# construction, not a boot any lock-cooperating script currently owns.
qemu_host_lock_acquire

# Clean up any previous QEMU
echo "[2/4] Starting QEMU..."
pkill -9 -f "qemu-system-aarch64.*kernel-aarch64" 2>/dev/null || true
Expand All @@ -89,8 +118,10 @@ if [ "$TEST_MODE" = "network" ]; then
fi

# Start QEMU in background
# Always include VirtIO GPU and keyboard so the kernel's MMIO enumeration
# discovers them (needed for interactive shell and device driver tests)
# VirtIO GPU and keyboard are included unconditionally so the kernel's MMIO
# enumeration discovers them (needed for interactive shell and device
# driver tests). qemu_host_lock_acquire already ran above, before the
# cleanup pkill.
qemu-system-aarch64 \
-M virt \
-cpu cortex-a72 \
Expand All @@ -104,6 +135,11 @@ qemu-system-aarch64 \
$NET_OPTS \
-serial "file:$SERIAL_OUTPUT" &
QEMU_PID=$!
# F2: registers QEMU with the lock's own EXIT trap (see
# docker/qemu/lib/qemu-host-lock.sh) so a SIGTERM/SIGINT delivered to just
# this script's own PID during the poll below still kills QEMU instead of
# orphaning it with the lock free.
qemu_host_lock_track_pid "$QEMU_PID"

# Wait for output - different markers for different test modes
echo "[3/4] Waiting for kernel output (${TIMEOUT_SECS}s timeout)..."
Expand Down
33 changes: 31 additions & 2 deletions scripts/run-arm64-graphics.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ BUILD_TYPE="${1:-release}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BREENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

# #826/#834/R181: this script's qemu-system-aarch64 boot runs behind the
# host-wide lock in docker/qemu/lib/qemu-host-lock.sh -- #834 extends that
# lock's coverage from docker/qemu/*.sh (its original #826/R181 scope) to
# scripts/ as well.
# shellcheck source=../docker/qemu/lib/qemu-host-lock.sh
source "$BREENIX_ROOT/docker/qemu/lib/qemu-host-lock.sh"

if [ "$BUILD_TYPE" = "debug" ]; then
KERNEL="$BREENIX_ROOT/target/aarch64-breenix-kernel/debug/kernel-aarch64"
else
Expand Down Expand Up @@ -86,7 +93,25 @@ esac
# - VirtIO net device (MMIO)
# - VirtIO keyboard device (MMIO) for keyboard input
# NOTE: Use -device virtio-*-device (MMIO) not virtio-*-pci
exec qemu-system-aarch64 \
# #834: this is an interactive display session (Ctrl-A X exits QEMU), but
# it still takes the host-wide lock -- not exempt just because a human, not
# a gate, is driving it. `exec` is dropped because replacing this shell's
# own process image would discard the qemu_host_lock_acquire EXIT trap
# before it could release the lock. A plain foreground launch (no `&`) is
# not a safe substitute for `exec`, though: a SIGTERM/SIGINT delivered to
# just this script's own PID does not propagate to a foreground child on
# its own, so the EXIT trap would run and release the lock while QEMU kept
# running, orphaned and untracked -- reproducing the exact unserialized
# double-boot contention this lock exists to prevent (#834 fix-round F1,
# 2026-09-05). QEMU is backgrounded instead, with its PID handed to
# qemu_host_lock_track_pid so the lock's own EXIT trap kills it before
# releasing the lock on that path, then `wait`ed on so this script still
# blocks in the foreground exactly as before. `0<&0` is required on the
# backgrounded launch: bash redirects a backgrounded command's stdin from
# /dev/null unless the command carries its own explicit stdin redirection,
# and this session's serial monitor needs this script's own stdin attached.
qemu_host_lock_acquire
qemu-system-aarch64 \
-M virt \
-cpu cortex-a72 \
-m 512M \
Expand All @@ -97,4 +122,8 @@ exec qemu-system-aarch64 \
-device virtio-net-device,netdev=net0 \
-netdev user,id=net0 \
-device virtio-keyboard-device \
-kernel "$KERNEL"
-kernel "$KERNEL" \
0<&0 &
QEMU_PID=$!
qemu_host_lock_track_pid "$QEMU_PID"
wait "$QEMU_PID"
13 changes: 13 additions & 0 deletions scripts/run-arm64-keyboard-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BREENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

# #826/#834/R181: this script's qemu-system-aarch64 boot runs behind the
# host-wide lock in docker/qemu/lib/qemu-host-lock.sh -- #834 extends that
# lock's coverage from docker/qemu/*.sh (its original #826/R181 scope) to
# scripts/ as well.
# shellcheck source=../docker/qemu/lib/qemu-host-lock.sh
source "$BREENIX_ROOT/docker/qemu/lib/qemu-host-lock.sh"

# Configuration
SERIAL_PORT=4454
MONITOR_PORT=4455
Expand Down Expand Up @@ -70,6 +77,7 @@ echo "[1/5] Starting QEMU..."

# Use file output for reliable logging + TCP for input
# The TCP serial allows bidirectional I/O - we'll connect and send input
qemu_host_lock_acquire
qemu-system-aarch64 \
-M virt -cpu cortex-a72 -m 512M \
-kernel "$KERNEL" \
Expand All @@ -82,6 +90,11 @@ qemu-system-aarch64 \
-monitor tcp:127.0.0.1:${MONITOR_PORT},server,nowait \
&
QEMU_PID=$!
# F2: registers QEMU with the lock's own EXIT trap (see
# docker/qemu/lib/qemu-host-lock.sh) so a SIGTERM/SIGINT delivered to just
# this script's own PID during the test loop below still kills QEMU instead
# of orphaning it with the lock free.
qemu_host_lock_track_pid "$QEMU_PID"

echo " QEMU started with PID $QEMU_PID"

Expand Down
33 changes: 31 additions & 2 deletions scripts/run-arm64-qemu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ BUILD_TYPE="${1:-release}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BREENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

# #826/#834/R181: this script's qemu-system-aarch64 boot runs behind the
# host-wide lock in docker/qemu/lib/qemu-host-lock.sh -- #834 extends that
# lock's coverage from docker/qemu/*.sh (its original #826/R181 scope) to
# scripts/ as well.
# shellcheck source=../docker/qemu/lib/qemu-host-lock.sh
source "$BREENIX_ROOT/docker/qemu/lib/qemu-host-lock.sh"

if [ "$BUILD_TYPE" = "debug" ]; then
KERNEL="$BREENIX_ROOT/target/aarch64-breenix-kernel/debug/kernel-aarch64"
else
Expand Down Expand Up @@ -96,7 +103,25 @@ else
DISPLAY_OPTS="-nographic"
fi

exec qemu-system-aarch64 \
# #834: this is an interactive display session (Ctrl-A X exits QEMU), but
# it still takes the host-wide lock -- not exempt just because a human, not
# a gate, is driving it. `exec` is dropped because replacing this shell's
# own process image would discard the qemu_host_lock_acquire EXIT trap
# before it could release the lock. A plain foreground launch (no `&`) is
# not a safe substitute for `exec`, though: a SIGTERM/SIGINT delivered to
# just this script's own PID does not propagate to a foreground child on
# its own, so the EXIT trap would run and release the lock while QEMU kept
# running, orphaned and untracked -- reproducing the exact unserialized
# double-boot contention this lock exists to prevent (#834 fix-round F1,
# 2026-09-05). QEMU is backgrounded instead, with its PID handed to
# qemu_host_lock_track_pid so the lock's own EXIT trap kills it before
# releasing the lock on that path, then `wait`ed on so this script still
# blocks in the foreground exactly as before. `0<&0` is required on the
# backgrounded launch: bash redirects a backgrounded command's stdin from
# /dev/null unless the command carries its own explicit stdin redirection,
# and this session's serial monitor needs this script's own stdin attached.
qemu_host_lock_acquire
qemu-system-aarch64 \
-M virt \
-cpu cortex-a72 \
-m 512M \
Expand All @@ -105,4 +130,8 @@ exec qemu-system-aarch64 \
-kernel "$KERNEL" \
$DISK_OPTS \
$NET_OPTS \
$DEBUG_OPTS
$DEBUG_OPTS \
0<&0 &
QEMU_PID=$!
qemu_host_lock_track_pid "$QEMU_PID"
wait "$QEMU_PID"
Loading