-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
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
--- /dev/null
+# 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/<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 |
--- /dev/null
+# 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
+```
--- /dev/null
+#!/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
--- /dev/null
+/*
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+
+/* ---- 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;
+}