From dc38eb27e7d0bd3591a176882ab503eb596830bb Mon Sep 17 00:00:00 2001 From: Ryan Breen Date: Fri, 23 Jan 2026 06:31:29 -0500 Subject: [PATCH] test(net): add Docker harness for nonblock EAGAIN integration test - Add nonblock_eagain_test feature to Cargo.toml and kernel/Cargo.toml - Add nonblock_eagain_test_main() to kernel main.rs for isolated test mode - Add run-nonblock-eagain-test.sh Docker harness script - Update kernel_main_continue cfg to exclude nonblock_eagain_test feature The test verifies SOCK_NONBLOCK creates a socket that returns EAGAIN immediately when no data is available, without blocking. Test results: - Nonblock EAGAIN test: PASS - Boot test: PASS Co-Authored-By: Claude Opus 4.5 --- Cargo.toml | 1 + docker/qemu/run-nonblock-eagain-test.sh | 134 ++++++++++++++++++++++++ kernel/Cargo.toml | 1 + kernel/src/main.rs | 69 +++++++++++- 4 files changed, 203 insertions(+), 2 deletions(-) create mode 100755 docker/qemu/run-nonblock-eagain-test.sh diff --git a/Cargo.toml b/Cargo.toml index 3567a6a..2093f9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ kthread_stress_test = ["kernel/kthread_stress_test"] # Run kthread stress test workqueue_test_only = ["kernel/workqueue_test_only"] # Run only workqueue tests and exit dns_test_only = ["kernel/dns_test_only"] # Run only DNS test and exit (fast network debugging) blocking_recv_test = ["kernel/blocking_recv_test"] # Run only blocking recvfrom test and exit +nonblock_eagain_test = ["kernel/nonblock_eagain_test"] # Run only nonblock EAGAIN test and exit test_divide_by_zero = ["kernel/test_divide_by_zero"] test_invalid_opcode = ["kernel/test_invalid_opcode"] test_page_fault = ["kernel/test_page_fault"] diff --git a/docker/qemu/run-nonblock-eagain-test.sh b/docker/qemu/run-nonblock-eagain-test.sh new file mode 100755 index 0000000..245e94d --- /dev/null +++ b/docker/qemu/run-nonblock-eagain-test.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# Run nonblock EAGAIN test in isolated Docker container +# Usage: ./run-nonblock-eagain-test.sh +# +# This script runs the nonblock_eagain_test kernel build in Docker. +# The test verifies that a nonblocking socket returns EAGAIN immediately +# when no data is available (no external packet needed). + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BREENIX_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Build Docker image if needed +IMAGE_NAME="breenix-qemu" +if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then + echo "Building Docker image..." + docker build -t "$IMAGE_NAME" "$SCRIPT_DIR" +fi + +echo "Building nonblock_eagain_test kernel..." +cargo build --release --features nonblock_eagain_test --bin qemu-uefi + +# Check for nonblock_eagain_test kernel build +UEFI_IMG=$(ls -t "$BREENIX_ROOT/target/release/build/breenix-"*/out/breenix-uefi.img 2>/dev/null | head -1) +if [ -z "$UEFI_IMG" ]; then + echo "Error: UEFI image not found." + exit 1 +fi + +if [ ! -f "$BREENIX_ROOT/target/test_binaries.img" ]; then + echo "Error: test_binaries.img not found. Build the test disk first." + exit 1 +fi + +# Create output directory +OUTPUT_DIR=$(mktemp -d) +trap "rm -rf $OUTPUT_DIR" EXIT + +# Create empty output files +touch "$OUTPUT_DIR/serial_kernel.txt" +touch "$OUTPUT_DIR/serial_user.txt" + +echo "Running nonblock EAGAIN test in Docker container..." +echo " UEFI image: $UEFI_IMG" +echo " Output dir: $OUTPUT_DIR" +echo "" + +# Copy OVMF files to writable location +cp "$BREENIX_ROOT/target/ovmf/x64/code.fd" "$OUTPUT_DIR/OVMF_CODE.fd" +cp "$BREENIX_ROOT/target/ovmf/x64/vars.fd" "$OUTPUT_DIR/OVMF_VARS.fd" + +# Run QEMU inside Docker +# No external packet needed - test verifies EAGAIN return immediately +timeout 60 docker run --rm \ + -v "$UEFI_IMG:/breenix/breenix-uefi.img:ro" \ + -v "$BREENIX_ROOT/target/test_binaries.img:/breenix/test_binaries.img:ro" \ + -v "$BREENIX_ROOT/target/ext2.img:/breenix/ext2.img:ro" \ + -v "$OUTPUT_DIR:/output" \ + "$IMAGE_NAME" \ + qemu-system-x86_64 \ + -pflash /output/OVMF_CODE.fd \ + -pflash /output/OVMF_VARS.fd \ + -drive if=none,id=hd,format=raw,media=disk,readonly=on,file=/breenix/breenix-uefi.img \ + -device virtio-blk-pci,drive=hd,bootindex=0,disable-modern=on,disable-legacy=off \ + -drive if=none,id=testdisk,format=raw,readonly=on,file=/breenix/test_binaries.img \ + -device virtio-blk-pci,drive=testdisk,disable-modern=on,disable-legacy=off \ + -drive if=none,id=ext2disk,format=raw,readonly=on,file=/breenix/ext2.img \ + -device virtio-blk-pci,drive=ext2disk,disable-modern=on,disable-legacy=off \ + -machine pc,accel=tcg \ + -cpu qemu64 \ + -smp 1 \ + -m 512 \ + -display none \ + -boot strict=on \ + -no-reboot \ + -no-shutdown \ + -monitor none \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ + -netdev user,id=net0 \ + -device e1000,netdev=net0,mac=52:54:00:12:34:56 \ + -serial file:/output/serial_user.txt \ + -serial file:/output/serial_kernel.txt \ + & + +QEMU_PID=$! + +echo "Waiting for nonblock EAGAIN test..." +TIMEOUT=45 +ELAPSED=0 + +while [ $ELAPSED -lt $TIMEOUT ]; do + sleep 1 + ELAPSED=$((ELAPSED + 1)) + + if [ -f "$OUTPUT_DIR/serial_user.txt" ]; then + USER_OUTPUT=$(cat "$OUTPUT_DIR/serial_user.txt" 2>/dev/null) + + if echo "$USER_OUTPUT" | grep -q "NONBLOCK_EAGAIN_TEST: PASS"; then + echo "" + echo "=========================================" + echo "NONBLOCK EAGAIN TEST: PASS" + echo "=========================================" + docker kill $(docker ps -q --filter ancestor="$IMAGE_NAME") 2>/dev/null || true + exit 0 + fi + + if echo "$USER_OUTPUT" | grep -q "NONBLOCK_EAGAIN_TEST:.*errno="; then + echo "" + echo "=========================================" + echo "NONBLOCK EAGAIN TEST: FAIL (wrong errno)" + echo "=========================================" + echo "" + echo "User output (COM1):" + cat "$OUTPUT_DIR/serial_user.txt" 2>/dev/null | grep -E "NONBLOCK_EAGAIN_TEST" || echo "(no output)" + docker kill $(docker ps -q --filter ancestor="$IMAGE_NAME") 2>/dev/null || true + exit 1 + fi + fi +done + +echo "" +echo "=========================================" +echo "NONBLOCK EAGAIN TEST: TIMEOUT" +echo "=========================================" +echo "" +echo "User output (COM1):" +cat "$OUTPUT_DIR/serial_user.txt" 2>/dev/null | grep -E "NONBLOCK_EAGAIN_TEST" || echo "(no nonblock eagain output)" +echo "" +echo "Last 20 lines of kernel output (COM2):" +tail -20 "$OUTPUT_DIR/serial_kernel.txt" 2>/dev/null || echo "(no output)" + +docker kill $(docker ps -q --filter ancestor="$IMAGE_NAME") 2>/dev/null || true +exit 1 diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 0f5ed20..ca22dba 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -23,6 +23,7 @@ kthread_stress_test = ["testing"] # Run kthread stress test (100+ kthreads) and workqueue_test_only = ["testing"] # Run only workqueue tests and exit dns_test_only = ["testing", "external_test_bins"] # Run only DNS test and exit (fast network debugging) blocking_recv_test = ["testing", "external_test_bins"] # Run only blocking recvfrom test and exit +nonblock_eagain_test = ["testing", "external_test_bins"] # Run only nonblock EAGAIN test and exit test_divide_by_zero = [] test_invalid_opcode = [] test_page_fault = [] diff --git a/kernel/src/main.rs b/kernel/src/main.rs index a6ce8a0..b836e3b 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -612,7 +612,7 @@ extern "C" fn kernel_main_on_kernel_stack(arg: *mut core::ffi::c_void) -> ! { // Continue with the rest of kernel initialization... // (This will include creating user processes, enabling interrupts, etc.) - #[cfg(not(any(feature = "kthread_stress_test", feature = "workqueue_test_only", feature = "dns_test_only", feature = "blocking_recv_test")))] + #[cfg(not(any(feature = "kthread_stress_test", feature = "workqueue_test_only", feature = "dns_test_only", feature = "blocking_recv_test", feature = "nonblock_eagain_test")))] kernel_main_continue(); // DNS_TEST_ONLY mode: Skip all other tests, just run dns_test @@ -622,6 +622,10 @@ extern "C" fn kernel_main_on_kernel_stack(arg: *mut core::ffi::c_void) -> ! { // BLOCKING_RECV_TEST mode: Skip all other tests, just run blocking_recv_test #[cfg(feature = "blocking_recv_test")] blocking_recv_test_main(); + + // NONBLOCK_EAGAIN_TEST mode: Skip all other tests, just run nonblock_eagain_test + #[cfg(feature = "nonblock_eagain_test")] + nonblock_eagain_test_main(); } /// DNS test only mode - minimal boot, just run DNS test and exit @@ -733,8 +737,69 @@ fn blocking_recv_test_main() -> ! { } } +/// Nonblock EAGAIN test only mode - minimal boot, just run nonblock_eagain_test and exit +#[cfg(feature = "nonblock_eagain_test")] +fn nonblock_eagain_test_main() -> ! { + use alloc::string::String; + + log::info!("=== NONBLOCK_EAGAIN_TEST: Starting minimal nonblock EAGAIN test ==="); + + // Create nonblock_eagain_test process + x86_64::instructions::interrupts::without_interrupts(|| { + serial_println!("NONBLOCK_EAGAIN_TEST: Loading nonblock_eagain_test binary"); + let elf = match userspace_test::load_test_binary_from_disk("nonblock_eagain_test") { + Ok(elf) => elf, + Err(e) => { + log::error!("NONBLOCK_EAGAIN_TEST: Failed to load nonblock_eagain_test: {}", e); + unsafe { + use x86_64::instructions::port::Port; + let mut port = Port::new(0xf4); + port.write(0x01u32); + } + return; + } + }; + match process::create_user_process(String::from("nonblock_eagain_test"), &elf) { + Ok(pid) => { + log::info!( + "NONBLOCK_EAGAIN_TEST: Created nonblock_eagain_test process with PID {}", + pid.as_u64() + ); + } + Err(e) => { + log::error!("NONBLOCK_EAGAIN_TEST: Failed to create nonblock_eagain_test: {}", e); + unsafe { + use x86_64::instructions::port::Port; + let mut port = Port::new(0xf4); + port.write(0x01u32); + } + } + } + }); + + // Enable interrupts so nonblock_eagain_test can run + log::info!("NONBLOCK_EAGAIN_TEST: Enabling interrupts"); + x86_64::instructions::interrupts::enable(); + + // Enter idle loop - nonblock_eagain_test will run via scheduler + // This test should complete quickly since it just verifies EAGAIN return + log::info!("NONBLOCK_EAGAIN_TEST: Entering idle loop (nonblock_eagain_test running via scheduler)"); + loop { + x86_64::instructions::interrupts::enable_and_hlt(); + + // Yield to give scheduler a chance + task::scheduler::yield_current(); + + // Poll for received packets (workaround for softirq timing) + net::process_rx(); + + // Drain loopback queue for localhost packets + net::drain_loopback_queue(); + } +} + /// Continue kernel initialization after setting up threading -#[cfg(not(any(feature = "kthread_stress_test", feature = "workqueue_test_only", feature = "dns_test_only", feature = "blocking_recv_test")))] +#[cfg(not(any(feature = "kthread_stress_test", feature = "workqueue_test_only", feature = "dns_test_only", feature = "blocking_recv_test", feature = "nonblock_eagain_test")))] fn kernel_main_continue() -> ! { // INTERACTIVE MODE: Load init_shell as the only userspace process #[cfg(feature = "interactive")]