From: Tulio A M Mendes Date: Tue, 10 Feb 2026 11:07:09 +0000 (-0300) Subject: feat: deep code audit + testing infrastructure (sparse, expect, host unit tests) X-Git-Url: https://projects.tadryanom.me/docs/static/gitweb.js?a=commitdiff_plain;h=cbfb2149709ba7646d2a9b3b0b998de3e122b9e4;p=AdrOS.git feat: deep code audit + testing infrastructure (sparse, expect, host unit tests) Deep Code Audit (docs/AUDIT_REPORT.md): - 18 findings across 4 categories: layer violations, logic/race conditions, security vulnerabilities, memory management - CRITICAL: user_range_ok weak default allows kernel addr access - CRITICAL: sigreturn allows IOPL escalation via eflags - CRITICAL: PMM bitmap has no locking (SMP race) - CRITICAL: file refcount manipulation not atomic - HIGH: slab allocator hal_mm_phys_to_virt can hit heap VA - HIGH: execve writes to user stack bypassing copy_to_user - Full summary table with severity, category, location Testing Infrastructure: - make check — cppcheck + sparse (kernel-oriented semantic checker) - make analyzer — gcc -fanalyzer (interprocedural analysis) - make test — QEMU + expect automated smoke test (19 checks) - make test-1cpu — single-CPU regression (50s timeout) - make test-host — 28 host-side unit tests for pure functions (itoa, itoa_hex, atoi, path_normalize, align) - make test-all — all of the above Testing Plan (docs/TESTING_PLAN.md): - Layer 1: Static analysis (cppcheck + sparse + gcc -fanalyzer) - Layer 2: QEMU + expect automated regression - Layer 3: QEMU + GDB scripted debugging (future) - Layer 4: Host-side unit tests for pure functions All tests passing: 19/19 smoke, 28/28 unit, cppcheck clean. --- diff --git a/Makefile b/Makefile index f7fc27b..0e922ba 100644 --- a/Makefile +++ b/Makefile @@ -141,11 +141,71 @@ run: iso -serial file:serial.log -monitor none -no-reboot -no-shutdown \ $(QEMU_DFLAGS) +# ---- Static Analysis ---- + cppcheck: @cppcheck --version >/dev/null @cppcheck --quiet --enable=warning,performance,portability --error-exitcode=1 \ -I include $(SRC_DIR) +# Sparse: kernel-oriented semantic checker (type safety, NULL, bitwise vs logical) +SPARSE_FLAGS := -m32 -D__i386__ -D__linux__ -Iinclude +SPARSE_SRCS := $(filter-out $(wildcard $(SRC_DIR)/arch/arm/*.c) \ + $(wildcard $(SRC_DIR)/arch/riscv/*.c) \ + $(wildcard $(SRC_DIR)/arch/mips/*.c) \ + $(wildcard $(SRC_DIR)/hal/arm/*.c) \ + $(wildcard $(SRC_DIR)/hal/riscv/*.c) \ + $(wildcard $(SRC_DIR)/hal/mips/*.c), $(C_SOURCES)) + +sparse: + @echo "[SPARSE] Running sparse on $(words $(SPARSE_SRCS)) files..." + @fail=0; \ + for f in $(SPARSE_SRCS); do \ + sparse $(SPARSE_FLAGS) $$f 2>&1; \ + done + @echo "[SPARSE] Done." + +# GCC -fanalyzer: interprocedural static analysis (use-after-free, NULL deref, etc) +ANALYZER_FLAGS := -m32 -ffreestanding -fanalyzer -fsyntax-only -Iinclude -O2 -Wno-cpp + +analyzer: + @echo "[ANALYZER] Running gcc -fanalyzer on $(words $(SPARSE_SRCS)) files..." + @fail=0; \ + for f in $(SPARSE_SRCS); do \ + $(CC) $(ANALYZER_FLAGS) $$f 2>&1 | grep -v "^$$" || true; \ + done + @echo "[ANALYZER] Done." + +# Combined static analysis: cppcheck + sparse +check: cppcheck sparse + @echo "[CHECK] All static analysis passed." + +# ---- Automated Smoke Test (QEMU + expect) ---- + +SMOKE_SMP ?= 4 +SMOKE_TIMEOUT ?= 40 + +test: iso + @echo "[TEST] Running smoke test (SMP=$(SMOKE_SMP), timeout=$(SMOKE_TIMEOUT)s)..." + @expect tests/smoke_test.exp $(SMOKE_SMP) $(SMOKE_TIMEOUT) + +test-1cpu: iso + @echo "[TEST] Running smoke test (SMP=1, timeout=50s)..." + @expect tests/smoke_test.exp 1 50 + +# ---- Host-Side Unit Tests ---- + +test-host: + @mkdir -p build/host + @echo "[TEST-HOST] Compiling tests/test_utils.c..." + @gcc -m32 -Wall -Wextra -Werror -Iinclude -o build/host/test_utils tests/test_utils.c + @./build/host/test_utils + +# ---- All Tests ---- + +test-all: check test-host test + @echo "[TEST-ALL] All tests passed." + scan-build: @command -v scan-build >/dev/null @scan-build --status-bugs $(MAKE) ARCH=$(ARCH) $(if $(CROSS),CROSS=$(CROSS),) all @@ -168,4 +228,4 @@ $(BUILD_DIR)/%.o: $(SRC_DIR)/%.S clean: rm -rf build $(KERNEL_NAME) -.PHONY: all clean iso run cppcheck scan-build mkinitrd-asan +.PHONY: all clean iso run cppcheck sparse analyzer check test test-1cpu test-host test-all scan-build mkinitrd-asan diff --git a/docs/AUDIT_REPORT.md b/docs/AUDIT_REPORT.md new file mode 100644 index 0000000..e1d245c --- /dev/null +++ b/docs/AUDIT_REPORT.md @@ -0,0 +1,310 @@ +# AdrOS Deep Code Audit Report + +## 1. Layer Violations (Arch-Dependent Code in Arch-Independent Files) + +### 1.1 CRITICAL — `src/kernel/syscall.c` uses x86 register names directly + +```c +// Line 18-20: #ifdef __i386__ with extern x86_sysenter_init +// Line 183: child_regs.eax = 0; +// Line 740-742: regs->eip, regs->useresp, regs->eax +// Line 1603+: regs->eax, regs->ebx, regs->ecx, regs->edx, regs->esi +// Line 1987-1991: syscall_init() with #if defined(__i386__) +``` + +**Impact**: `syscall.c` is in `src/kernel/` (arch-independent) but references x86 register +names (`eax`, `ebx`, `ecx`, `edx`, `esi`, `eip`, `useresp`) throughout the entire +`syscall_handler()` function. This makes the file completely non-portable. + +**Fix**: Define a `struct syscall_args` in a HAL header with generic field names +(`arg0`–`arg5`, `result`, `ip`, `sp`), and have each arch's interrupt stub populate it. + +### 1.2 MODERATE — `src/mm/heap.c` hardcodes x86 virtual address + +```c +// Line 12: #define KHEAP_START 0xD0000000 +``` + +**Impact**: The heap start address `0xD0000000` is specific to the x86 higher-half kernel +layout. ARM/RISC-V/MIPS would use different virtual address ranges. + +**Fix**: Move `KHEAP_START` to a per-arch `include/arch//mm_layout.h` or define it +via the HAL (`hal_mm_heap_start()`). + +### 1.3 MODERATE — `include/interrupts.h` conditionally includes x86 IDT header + +```c +// Line 6-7: #if defined(__i386__) ... #include "arch/x86/idt.h" +``` + +**Impact**: The generic `interrupts.h` pulls in the full x86 `struct registers` (with +`eax`, `ebx`, etc.) into every file that includes `process.h`. This is the root cause +of 1.1 — the x86 register layout leaks into all kernel code. + +**Fix**: Define a generic `struct trap_frame` in `interrupts.h` with arch-neutral names. +Each arch provides its own mapping. + +### 1.4 LOW — `include/io.h` includes `arch/x86/io.h` unconditionally + +```c +// The generic io.h provides MMIO helpers but also pulls in x86 port I/O +``` + +**Impact**: Non-x86 architectures don't have port I/O. The `#include "arch/x86/io.h"` is +guarded by `#if defined(__i386__)` so it compiles, but the design leaks. + +### 1.5 LOW — `src/kernel/syscall.c` line 994: `strcpy(s, tmp)` in `path_normalize_inplace` + +The function writes to a local `char tmp[128]` then copies back with `strcpy(s, tmp)`. +If `s` is shorter than 128 bytes, this is a buffer overflow. Currently `s` is always +128 bytes (from `process.cwd` or local buffers), but the function signature doesn't +enforce this. + +--- + +## 2. Logic Errors and Race Conditions + +### 2.1 CRITICAL — PMM `pmm_alloc_page` / `pmm_free_page` have no locking + +```c +// src/mm/pmm.c: No spinlock protects the bitmap or frame_refcount arrays +``` + +**Impact**: On SMP (4 CPUs active), concurrent `pmm_alloc_page()` calls can return the +same physical frame to two callers, causing memory corruption. `pmm_free_page()` can +corrupt `used_memory` counter. `pmm_incref`/`pmm_decref` use atomics for refcount but +`bitmap_set`/`bitmap_unset` are NOT atomic — byte-level read-modify-write races. + +**Fix**: Add a `spinlock_t pmm_lock` and wrap `pmm_alloc_page`, `pmm_free_page`, +`pmm_mark_region` with `spin_lock_irqsave`/`spin_unlock_irqrestore`. + +### 2.2 CRITICAL — `file->refcount` manipulation is not atomic + +```c +// src/kernel/syscall.c line 197: f->refcount++; (no lock) +// src/kernel/syscall.c line 762: f->refcount++; (no lock) +// src/kernel/scheduler.c line 148: f->refcount--; (under sched_lock, but other +// increments happen outside sched_lock) +``` + +**Impact**: `f->refcount++` in `syscall_fork_impl` and `syscall_dup2_impl` runs without +any lock. If a timer interrupt fires and `schedule()` runs `process_close_all_files_locked` +concurrently, the refcount can go negative or skip zero, leaking file descriptors or +causing use-after-free. + +**Fix**: Use `__sync_fetch_and_add` / `__sync_sub_and_fetch` for refcount, or protect +all refcount manipulation with a dedicated `files_lock`. + +### 2.3 HIGH — Slab allocator uses `hal_mm_phys_to_virt` without VMM mapping + +```c +// src/mm/slab.c line 39: uint8_t* vbase = (uint8_t*)hal_mm_phys_to_virt(...) +``` + +**Impact**: Same bug as the DMA heap collision. If `pmm_alloc_page()` returns a physical +address above 16MB, `hal_mm_phys_to_virt` adds `0xC0000000` which can land in the heap +VA range (`0xD0000000`+) or other mapped regions. The slab then corrupts heap memory. + +**Fix**: Either allocate slab pages from the heap (`kmalloc(PAGE_SIZE)`), or use +`vmm_map_page` to map at a dedicated VA range (like the DMA fix). + +### 2.4 HIGH — `process_waitpid` loop can miss NULL in circular list + +```c +// src/kernel/scheduler.c line 267: } while (it != start); +``` + +**Impact**: The inner loop `do { ... } while (it != start)` doesn't check `it != NULL` +before dereferencing. If the circular list is broken (e.g., a process was reaped +concurrently), this causes a NULL pointer dereference. + +### 2.5 MODERATE — `schedule()` unlocks spinlock before `context_switch` + +```c +// src/kernel/scheduler.c line 621: spin_unlock_irqrestore(&sched_lock, irq_flags); +// line 623: context_switch(&prev->sp, current_process->sp); +``` + +**Impact**: Between unlock and context_switch, another CPU can modify `current_process` +or the process being switched to. On SMP this is a race window. Currently only BSP runs +the scheduler, so this is latent. + +### 2.6 MODERATE — `itoa` has no buffer size parameter + +```c +// src/kernel/utils.c line 64: void itoa(int num, char* str, int base) +``` + +**Impact**: `itoa` writes to `str` without knowing its size. For `INT_MIN` in base 10, +it writes 12 characters (`-2147483648\0`). Many call sites use `char tmp[12]` which is +exactly enough, but `char tmp[11]` would overflow. No safety margin. + +### 2.7 MODERATE — `itoa` undefined behavior for `INT_MIN` + +```c +// src/kernel/utils.c line 77: num = -num; +``` + +**Impact**: When `num == INT_MIN` (-2147483648), `-num` is undefined behavior in C +(signed integer overflow). On x86 with two's complement it happens to work, but it's +technically UB. + +### 2.8 LOW — `pmm_alloc_page_low` in VMM wastes pages + +```c +// src/arch/x86/vmm.c line 59-71: pmm_alloc_page_low() +``` + +**Impact**: Allocates pages and immediately frees them if above 16MB. Under memory +pressure this can loop 1024 times, freeing pages that other CPUs might need. Not a +correctness bug but a performance/reliability issue. + +--- + +## 3. Security Vulnerabilities + +### 3.1 CRITICAL — Weak `user_range_ok` allows kernel memory access + +```c +// src/kernel/uaccess.c line 17-24: +int user_range_ok(const void* user_ptr, size_t len) { + uintptr_t uaddr = (uintptr_t)user_ptr; + if (len == 0) return 1; + if (uaddr == 0) return 0; + uintptr_t end = uaddr + len - 1; + if (end < uaddr) return 0; // overflow check + return 1; // <-- ALWAYS returns 1 for non-NULL, non-overflow! +} +``` + +**Impact**: This function does NOT check that the address is in user space (below +`0xC0000000` on x86). A malicious userspace program can pass kernel addresses +(e.g., `0xC0100000`) to `read()`/`write()` syscalls and read/write arbitrary kernel +memory. **This is a privilege escalation vulnerability.** + +The x86-specific override in `src/arch/x86/uaccess.c` may fix this, but the weak +default is dangerous if the override is not linked. + +**Fix**: The weak default must reject addresses >= `KERNEL_VIRT_BASE`. Better: always +link the arch-specific version. + +### 3.2 CRITICAL — `sigreturn` allows arbitrary register restoration + +```c +// src/kernel/syscall.c line 1380-1398: syscall_sigreturn_impl +``` + +**Impact**: The sigreturn syscall restores ALL registers from a user-provided +`sigframe`. While it checks `cs & 3 == 3` and `ss & 3 == 3`, it does NOT validate +`eflags`. A user can set `IOPL=3` in the saved eflags, gaining direct port I/O access +from ring 3. This allows arbitrary hardware access. + +**Fix**: Mask eflags: `f.saved.eflags = (f.saved.eflags & ~0x3000) | 0x200;` (clear +IOPL, ensure IF). + +### 3.3 HIGH — `execve` writes to user stack via kernel pointer + +```c +// src/kernel/syscall.c line 697: memcpy((void*)sp, kenvp[i], len); +// line 705: memcpy((void*)sp, kargv[i], len); +// line 714: memcpy((void*)sp, envp_ptrs_va, ...); +// line 724: *(uint32_t*)sp = (uint32_t)argc; +``` + +**Impact**: After `vmm_as_activate(new_as)`, the code writes directly to user-space +addresses via `memcpy((void*)sp, ...)`. This bypasses `copy_to_user` and its +validation. If the new address space mapping is incorrect, this could write to +kernel memory. + +### 3.4 HIGH — No SMEP/SMAP enforcement + +**Impact**: The kernel doesn't enable SMEP (Supervisor Mode Execution Prevention) or +SMAP (Supervisor Mode Access Prevention) even if the CPU supports them. Without SMEP, +a kernel exploit can jump to user-mapped code. Without SMAP, accidental kernel reads +from user pointers succeed silently. + +### 3.5 MODERATE — `fd_get` doesn't validate fd bounds everywhere + +Many syscall implementations call `fd_get(fd)` but some paths (like the early +`fd == 1 || fd == 2` check in `syscall_write_impl`) access `current_process->files[fd]` +directly without bounds checking `fd < PROCESS_MAX_FILES`. + +### 3.6 MODERATE — `path_normalize_inplace` doesn't prevent path traversal to kernel FS + +The path normalization resolves `..` but doesn't prevent accessing sensitive kernel +mount points. A user can `open("/proc/self/status")` which is fine, but there's no +permission model — any process can read any file. + +--- + +## 4. Memory Management Issues + +### 4.1 HIGH — Heap never grows + +```c +// src/mm/heap.c: KHEAP_INITIAL_SIZE = 10MB, no growth mechanism +``` + +**Impact**: The kernel heap is fixed at 10MB. Once exhausted, all `kmalloc` calls fail. +There's no mechanism to map additional pages and extend the heap. For a kernel with +many processes, 10MB can be tight. + +### 4.2 MODERATE — `kfree` doesn't zero freed memory + +**Impact**: Freed heap blocks retain their old contents. If a new allocation reuses the +block, it may contain sensitive data from a previous allocation (information leak +between processes via kernel allocations). + +### 4.3 MODERATE — No stack guard pages + +Kernel stacks are 4KB (`kmalloc(4096)`) with no guard page below. A stack overflow +silently corrupts the heap header of the adjacent allocation. + +--- + +## 5. Miscellaneous Issues + +### 5.1 `proc_meminfo_read` reads `ready_queue_head` without lock + +```c +// src/kernel/procfs.c line 108-114: iterates process list without sched_lock +``` + +### 5.2 `process_kill` self-kill path calls `schedule()` without lock + +```c +// src/kernel/scheduler.c line 166-169: process_exit_notify + schedule without lock +``` + +### 5.3 `tmpfs_node_alloc` uses unbounded `strcpy` + +```c +// src/kernel/tmpfs.c line 29: strcpy(n->vfs.name, name); +``` + +If `name` exceeds 128 bytes (the size of `fs_node.name`), this overflows. + +--- + +## 6. Summary Table + +| # | Severity | Category | Location | Description | +|---|----------|----------|----------|-------------| +| 3.1 | CRITICAL | Security | uaccess.c | user_range_ok allows kernel addr | +| 3.2 | CRITICAL | Security | syscall.c | sigreturn allows IOPL escalation | +| 2.1 | CRITICAL | Race | pmm.c | No locking on PMM bitmap | +| 2.2 | CRITICAL | Race | syscall.c | file refcount not atomic | +| 1.1 | CRITICAL | Layer | syscall.c | x86 registers in generic code | +| 2.3 | HIGH | Memory | slab.c | phys_to_virt can hit heap VA | +| 3.3 | HIGH | Security | syscall.c | execve bypasses copy_to_user | +| 3.4 | HIGH | Security | - | No SMEP/SMAP | +| 4.1 | HIGH | Memory | heap.c | Heap never grows | +| 2.4 | HIGH | Logic | scheduler.c | waitpid NULL deref risk | +| 1.2 | MODERATE | Layer | heap.c | Hardcoded heap VA | +| 1.3 | MODERATE | Layer | interrupts.h | x86 registers leak | +| 2.5 | MODERATE | Race | scheduler.c | Unlock before context_switch | +| 2.6 | MODERATE | Logic | utils.c | itoa no buffer size | +| 2.7 | MODERATE | Logic | utils.c | itoa UB for INT_MIN | +| 3.5 | MODERATE | Security | syscall.c | fd bounds not always checked | +| 4.2 | MODERATE | Memory | heap.c | kfree doesn't zero | +| 4.3 | MODERATE | Memory | scheduler.c | No stack guard pages | diff --git a/docs/TESTING_PLAN.md b/docs/TESTING_PLAN.md new file mode 100644 index 0000000..582cb8e --- /dev/null +++ b/docs/TESTING_PLAN.md @@ -0,0 +1,96 @@ +# AdrOS Testing Infrastructure Plan + +## Current State + +- **Smoke test**: `make run` + `grep serial.log` (manual, fragile) +- **Static analysis**: `cppcheck` (basic warnings only) +- **No unit tests, no automated regression, no structured test harness** + +## Available Tools (already installed) + +| Tool | Path | Purpose | +|------|------|---------| +| cppcheck | /usr/bin/cppcheck | Static analysis (already in use) | +| sparse | /usr/bin/sparse | Kernel-oriented static analysis (C semantics, type checking) | +| gcc | /usr/bin/gcc | Compiler with `-fsanitize`, `-fanalyzer` | +| qemu-system-i386 | /usr/bin/qemu-system-i386 | Emulation + smoke tests | +| gdb | /usr/bin/gdb | Debugging via QEMU `-s -S` | +| expect | /usr/bin/expect | Scripted QEMU serial interaction | +| python3 | /usr/bin/python3 | Test runner orchestration | + +## Proposed Testing Layers + +### Layer 1: Enhanced Static Analysis (`make check`) + +**Tools**: cppcheck + sparse + gcc -fanalyzer + +- **cppcheck**: Already in use. Keep as-is. +- **sparse**: Kernel-focused. Catches `__user` pointer misuse, bitwise vs logical + confusion, type width issues. Run with `C=1` or as a standalone pass. +- **gcc -fanalyzer**: Interprocedural static analysis. Catches use-after-free, + double-free, NULL deref paths, buffer overflows across function boundaries. + +**Implementation**: Single `make check` target that runs all three. + +### Layer 2: QEMU + expect Automated Regression (`make test`) + +**Tools**: expect (or Python pexpect) + QEMU serial + +This replaces the manual `grep serial.log` approach with a scripted test that: +1. Boots QEMU with serial output to a PTY +2. Waits for specific strings in order (with timeouts) +3. Reports PASS/FAIL per test case +4. Detects PANIC, OOM, or unexpected output + +**Why expect over Unity/KUnit**: +- **Unity** requires linking a test framework into the kernel binary, which changes + the binary layout and can mask bugs. It also requires a host-side test runner. +- **KUnit** is Linux-specific and not applicable to a custom kernel. +- **expect** tests the actual kernel binary as-is, catching real boot/runtime bugs + that unit tests would miss. It's the right tool for an OS kernel. + +**Implementation**: `tests/smoke_test.exp` expect script + `make test` target. + +### Layer 3: QEMU + GDB Scripted Debugging (`make test-gdb`) + +**Tools**: QEMU `-s -S` + GDB with Python scripting + +For targeted regression tests that need to inspect kernel state: +- Verify page table entries after VMM operations +- Check PMM bitmap consistency +- Validate heap integrity after stress allocation +- Breakpoint on `uart_print("[HEAP] Corruption")` to catch corruption early + +**Implementation**: `tests/gdb_checks.py` GDB Python script + `make test-gdb` target. + +### Layer 4: Host-Side Unit Tests for Pure Functions (`make test-host`) + +**Tools**: Simple C test harness (no external framework needed) + +Some kernel functions are pure computation with no hardware dependency: +- `itoa`, `itoa_hex`, `atoi`, `strlen`, `strcmp`, `memcpy`, `memset` +- `path_normalize_inplace` (critical for security) +- `align_up`, `align_down` +- Bitmap operations + +These can be compiled and run on the host with `gcc -m32` and a minimal test harness. +No need for Unity — a simple `assert()` + `main()` is sufficient for a kernel project. + +**Implementation**: `tests/test_utils.c` compiled with host gcc. + +## Recommended Implementation Order + +1. **Layer 1** (sparse + gcc -fanalyzer) — immediate value, zero runtime cost +2. **Layer 2** (expect smoke test) — replaces manual grep, catches regressions +3. **Layer 4** (host unit tests) — catches logic bugs in pure functions +4. **Layer 3** (GDB scripted) — for deep debugging, lower priority + +## Makefile Targets + +```makefile +make check # cppcheck + sparse + gcc -fanalyzer +make test # QEMU + expect automated smoke test +make test-host # Host-side unit tests for pure functions +make test-gdb # QEMU + GDB scripted checks (optional) +make test-all # All of the above +``` diff --git a/tests/smoke_test.exp b/tests/smoke_test.exp new file mode 100755 index 0000000..3ad5749 --- /dev/null +++ b/tests/smoke_test.exp @@ -0,0 +1,186 @@ +#!/usr/bin/expect -f +# +# AdrOS Automated Smoke Test via QEMU serial console +# +# Usage: expect tests/smoke_test.exp [smp_count] [timeout_sec] +# smp_count : number of CPUs (default: 4) +# timeout_sec: max seconds to wait for full boot (default: 30) +# +# Exit codes: +# 0 = all checks passed +# 1 = test failure (missing expected output or PANIC detected) +# 2 = timeout (boot did not complete in time) + +set smp [lindex $argv 0] +if {$smp eq ""} { set smp 4 } + +set timeout_sec [lindex $argv 1] +if {$timeout_sec eq ""} { set timeout_sec 30 } + +set iso "adros-x86.iso" +set disk "disk.img" +set serial_log "serial.log" + +# Ensure disk image exists +if {![file exists $disk]} { + exec dd if=/dev/zero of=$disk bs=1M count=4 2>/dev/null +} + +# Remove old serial log +file delete -force $serial_log + +# Start QEMU in background, serial to file +set qemu_pid [exec qemu-system-i386 \ + -smp $smp -boot d -cdrom $iso -m 128M -display none \ + -drive file=$disk,if=ide,format=raw \ + -serial file:$serial_log -monitor none \ + -no-reboot -no-shutdown &] + +# Wait for QEMU to start writing +after 1000 + +# ---- Test definitions ---- +# Each test is {description pattern} +set tests { + {"Heap init" "\\[HEAP\\] 10MB Heap Ready."} + {"PCI enumeration" "\\[PCI\\] Enumerated"} + {"ATA DMA init" "\\[ATA-DMA\\] Initialized"} + {"ATA DMA mode" "\\[ATA\\] Using DMA mode."} + {"SMP CPUs active" "CPU\\(s\\) active."} + {"User ring3 entry" "\\[USER\\] enter ring3"} + {"init.elf hello" "\\[init\\] hello from init.elf"} + {"open/read/close" "\\[init\\] open/read/close OK"} + {"overlay copy-up" "\\[init\\] overlay copy-up OK"} + {"lseek/stat/fstat" "\\[init\\] lseek/stat/fstat OK"} + {"dup2 restore" "\\[init\\] dup2 restore tty OK"} + {"kill SIGKILL" "\\[init\\] kill\\(SIGKILL\\) OK"} + {"poll pipe" "\\[init\\] poll\\(pipe\\) OK"} + {"select pipe" "\\[init\\] select\\(pipe\\) OK"} + {"persist counter" "\\[init\\] /persist/counter="} + {"dev tty write" "\\[init\\] /dev/tty write OK"} + {"diskfs test" "\\[init\\] /disk/test prev="} + {"diskfs mkdir/unlink" "\\[init\\] diskfs mkdir/unlink OK"} + {"diskfs getdents" "\\[init\\] diskfs getdents OK"} +} + +# ---- Poll serial.log for results ---- +set start_time [clock seconds] +set passed 0 +set failed 0 +set panic 0 +set total [llength $tests] + +# Track which tests have passed +for {set i 0} {$i < $total} {incr i} { + set test_passed($i) 0 +} + +proc check_log {} { + global serial_log + if {[file exists $serial_log]} { + set fd [open $serial_log r] + set content [read $fd] + close $fd + return $content + } + return "" +} + +# Poll loop +while {1} { + set elapsed [expr {[clock seconds] - $start_time}] + if {$elapsed > $timeout_sec} { + break + } + + set log [check_log] + + # Check for PANIC + if {[regexp {KERNEL PANIC} $log]} { + set panic 1 + break + } + + # Check for HEAP OOM + if {[regexp {\[HEAP\] OOM} $log]} { + set panic 1 + break + } + + # Check each test + set all_done 1 + for {set i 0} {$i < $total} {incr i} { + if {$test_passed($i)} continue + set pattern [lindex [lindex $tests $i] 1] + if {[regexp $pattern $log]} { + set test_passed($i) 1 + } else { + set all_done 0 + } + } + + if {$all_done} { + break + } + + after 1000 +} + +# Kill QEMU +catch {exec pkill -f "qemu-system-i386.*$iso" 2>/dev/null} +after 500 + +# ---- Report results ---- +puts "" +puts "=========================================" +puts " AdrOS Smoke Test Results (SMP=$smp)" +puts "=========================================" + +if {$panic} { + set log [check_log] + puts "" + puts " *** KERNEL PANIC DETECTED ***" + if {[regexp {Exception Number: (\d+)} $log _ exc]} { + puts " Exception: $exc" + } + if {[regexp {EIP: (0x[0-9A-Fa-f]+)} $log _ eip]} { + puts " EIP: $eip" + } + if {[regexp {PAGE FAULT at address: (0x[0-9A-Fa-f]+)} $log _ addr]} { + puts " Fault address: $addr" + } + puts "" +} + +for {set i 0} {$i < $total} {incr i} { + set desc [lindex [lindex $tests $i] 0] + if {$test_passed($i)} { + puts " PASS $desc" + incr passed + } else { + puts " FAIL $desc" + incr failed + } +} + +set elapsed [expr {[clock seconds] - $start_time}] +puts "" +puts " $passed/$total passed, $failed failed ($elapsed sec)" + +if {$panic} { + puts " RESULT: FAIL (PANIC)" + exit 1 +} + +if {$failed > 0} { + set elapsed_val [expr {[clock seconds] - $start_time}] + if {$elapsed_val >= $timeout_sec} { + puts " RESULT: FAIL (TIMEOUT after ${timeout_sec}s)" + exit 2 + } + puts " RESULT: FAIL" + exit 1 +} + +puts " RESULT: PASS" +exit 0 diff --git a/tests/test_utils.c b/tests/test_utils.c new file mode 100644 index 0000000..0a10474 --- /dev/null +++ b/tests/test_utils.c @@ -0,0 +1,400 @@ +/* + * AdrOS Host-Side Unit Tests for Pure Functions + * + * Compile: gcc -m32 -Iinclude -o build/test_utils tests/test_utils.c + * Run: ./build/test_utils + * + * Tests kernel utility functions that have no hardware dependency. + */ + +#include +#include +#include +#include + +/* ---- Minimal test framework ---- */ +static int g_tests_run = 0; +static int g_tests_passed = 0; +static int g_tests_failed = 0; + +#define TEST(name) static void test_##name(void) +#define RUN(name) do { \ + g_tests_run++; \ + printf(" %-40s ", #name); \ + test_##name(); \ + printf("PASS\n"); \ + g_tests_passed++; \ +} while(0) + +#define ASSERT_EQ(a, b) do { \ + if ((a) != (b)) { \ + printf("FAIL\n %s:%d: %d != %d\n", __FILE__, __LINE__, (int)(a), (int)(b)); \ + g_tests_failed++; \ + return; \ + } \ +} while(0) + +#define ASSERT_STR_EQ(a, b) do { \ + if (strcmp((a), (b)) != 0) { \ + printf("FAIL\n %s:%d: \"%s\" != \"%s\"\n", __FILE__, __LINE__, (a), (b)); \ + g_tests_failed++; \ + return; \ + } \ +} while(0) + +/* ---- Functions under test (copied from kernel sources) ---- */ + +/* From src/kernel/utils.c */ +static void reverse(char* str, int length) { + int start = 0; + int end = length - 1; + while (start < end) { + char temp = str[start]; + str[start] = str[end]; + str[end] = temp; + start++; + end--; + } +} + +static void itoa(int num, char* str, int base) { + int i = 0; + int isNegative = 0; + + if (num == 0) { + str[i++] = '0'; + str[i] = '\0'; + return; + } + + if (num < 0 && base == 10) { + isNegative = 1; + num = -num; + } + + while (num != 0) { + int rem = num % base; + str[i++] = (rem > 9) ? (rem - 10) + 'a' : rem + '0'; + num = num / base; + } + + if (isNegative) + str[i++] = '-'; + + str[i] = '\0'; + reverse(str, i); +} + +static int atoi_k(const char* str) { + int res = 0; + int sign = 1; + int i = 0; + + if (str[0] == '-') { + sign = -1; + i++; + } + + for (; str[i] != '\0'; ++i) { + if (str[i] >= '0' && str[i] <= '9') + res = res * 10 + str[i] - '0'; + } + + return sign * res; +} + +static void itoa_hex(uint32_t num, char* str) { + const char hex_chars[] = "0123456789ABCDEF"; + str[0] = '0'; + str[1] = 'x'; + + for (int i = 0; i < 8; i++) { + str[9 - i] = hex_chars[num & 0xF]; + num >>= 4; + } + str[10] = '\0'; +} + +/* From src/kernel/syscall.c — path_normalize_inplace */ +static void path_normalize_inplace(char* s) { + if (!s) return; + if (s[0] == 0) { + strcpy(s, "/"); + return; + } + + char tmp[128]; + size_t comp_start[32]; + int depth = 0; + size_t w = 0; + + const char* p = s; + int absolute = (*p == '/'); + if (absolute) { + tmp[w++] = '/'; + while (*p == '/') p++; + } + + while (*p != 0) { + const char* seg = p; + while (*p != 0 && *p != '/') p++; + size_t seg_len = (size_t)(p - seg); + while (*p == '/') p++; + + if (seg_len == 1 && seg[0] == '.') { + continue; + } + + if (seg_len == 2 && seg[0] == '.' && seg[1] == '.') { + if (depth > 0) { + depth--; + w = comp_start[depth]; + } + continue; + } + + if (depth < 32) { + comp_start[depth++] = w; + } + + if (w > 1 || (w == 1 && tmp[0] != '/')) { + if (w + 1 < sizeof(tmp)) tmp[w++] = '/'; + } + + for (size_t i = 0; i < seg_len && w + 1 < sizeof(tmp); i++) { + tmp[w++] = seg[i]; + } + } + + if (w == 0) { + tmp[w++] = '/'; + } + + while (w > 1 && tmp[w - 1] == '/') { + w--; + } + + tmp[w] = 0; + strcpy(s, tmp); +} + +/* From src/mm/pmm.c — align helpers */ +static uint64_t align_down(uint64_t value, uint64_t align) { + return value & ~(align - 1); +} + +static uint64_t align_up(uint64_t value, uint64_t align) { + return (value + align - 1) & ~(align - 1); +} + +/* ======== TESTS ======== */ + +/* --- itoa tests --- */ +TEST(itoa_zero) { + char buf[16]; + itoa(0, buf, 10); + ASSERT_STR_EQ(buf, "0"); +} + +TEST(itoa_positive) { + char buf[16]; + itoa(12345, buf, 10); + ASSERT_STR_EQ(buf, "12345"); +} + +TEST(itoa_negative) { + char buf[16]; + itoa(-42, buf, 10); + ASSERT_STR_EQ(buf, "-42"); +} + +TEST(itoa_hex_base) { + char buf[16]; + itoa(255, buf, 16); + ASSERT_STR_EQ(buf, "ff"); +} + +TEST(itoa_one) { + char buf[16]; + itoa(1, buf, 10); + ASSERT_STR_EQ(buf, "1"); +} + +TEST(itoa_large) { + char buf[16]; + itoa(2147483647, buf, 10); + ASSERT_STR_EQ(buf, "2147483647"); +} + +/* --- itoa_hex tests --- */ +TEST(itoa_hex_zero) { + char buf[16]; + itoa_hex(0, buf); + ASSERT_STR_EQ(buf, "0x00000000"); +} + +TEST(itoa_hex_deadbeef) { + char buf[16]; + itoa_hex(0xDEADBEEF, buf); + ASSERT_STR_EQ(buf, "0xDEADBEEF"); +} + +TEST(itoa_hex_small) { + char buf[16]; + itoa_hex(0xFF, buf); + ASSERT_STR_EQ(buf, "0x000000FF"); +} + +/* --- atoi tests --- */ +TEST(atoi_zero) { + ASSERT_EQ(atoi_k("0"), 0); +} + +TEST(atoi_positive) { + ASSERT_EQ(atoi_k("12345"), 12345); +} + +TEST(atoi_negative) { + ASSERT_EQ(atoi_k("-99"), -99); +} + +TEST(atoi_leading_garbage) { + /* atoi_k skips non-digit chars after optional sign */ + ASSERT_EQ(atoi_k("abc"), 0); +} + +/* --- path_normalize_inplace tests --- */ +TEST(path_root) { + char p[128] = "/"; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/"); +} + +TEST(path_empty) { + char p[128] = ""; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/"); +} + +TEST(path_simple) { + char p[128] = "/foo/bar"; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/foo/bar"); +} + +TEST(path_trailing_slash) { + char p[128] = "/foo/bar/"; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/foo/bar"); +} + +TEST(path_double_slash) { + char p[128] = "/foo//bar"; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/foo/bar"); +} + +TEST(path_dot) { + char p[128] = "/foo/./bar"; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/foo/bar"); +} + +TEST(path_dotdot) { + char p[128] = "/foo/bar/../baz"; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/foo/baz"); +} + +TEST(path_dotdot_root) { + char p[128] = "/foo/.."; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/"); +} + +TEST(path_dotdot_beyond_root) { + char p[128] = "/../.."; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/"); +} + +TEST(path_complex) { + char p[128] = "/a/b/c/../../d/./e/../f"; + path_normalize_inplace(p); + ASSERT_STR_EQ(p, "/a/d/f"); +} + +/* --- align tests --- */ +TEST(align_down_basic) { + ASSERT_EQ(align_down(4097, 4096), 4096); +} + +TEST(align_down_exact) { + ASSERT_EQ(align_down(4096, 4096), 4096); +} + +TEST(align_up_basic) { + ASSERT_EQ(align_up(4097, 4096), 8192); +} + +TEST(align_up_exact) { + ASSERT_EQ(align_up(4096, 4096), 4096); +} + +TEST(align_up_zero) { + ASSERT_EQ(align_up(0, 4096), 0); +} + +/* ======== MAIN ======== */ +int main(void) { + printf("\n=========================================\n"); + printf(" AdrOS Host-Side Unit Tests\n"); + printf("=========================================\n\n"); + + /* itoa */ + RUN(itoa_zero); + RUN(itoa_positive); + RUN(itoa_negative); + RUN(itoa_hex_base); + RUN(itoa_one); + RUN(itoa_large); + + /* itoa_hex */ + RUN(itoa_hex_zero); + RUN(itoa_hex_deadbeef); + RUN(itoa_hex_small); + + /* atoi */ + RUN(atoi_zero); + RUN(atoi_positive); + RUN(atoi_negative); + RUN(atoi_leading_garbage); + + /* path_normalize */ + RUN(path_root); + RUN(path_empty); + RUN(path_simple); + RUN(path_trailing_slash); + RUN(path_double_slash); + RUN(path_dot); + RUN(path_dotdot); + RUN(path_dotdot_root); + RUN(path_dotdot_beyond_root); + RUN(path_complex); + + /* align */ + RUN(align_down_basic); + RUN(align_down_exact); + RUN(align_up_basic); + RUN(align_up_exact); + RUN(align_up_zero); + + printf("\n %d/%d passed, %d failed\n", g_tests_passed, g_tests_run, g_tests_failed); + + if (g_tests_failed > 0) { + printf(" RESULT: FAIL\n\n"); + return 1; + } + printf(" RESULT: PASS\n\n"); + return 0; +}