Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
134 changes: 134 additions & 0 deletions docker/qemu/run-nonblock-eagain-test.sh
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions kernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
69 changes: 67 additions & 2 deletions kernel/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")]
Expand Down
Loading