From: Tulio A M Mendes Date: Tue, 10 Feb 2026 11:44:55 +0000 (-0300) Subject: test: expand test suite with security unit tests and GDB scripted checks X-Git-Url: https://projects.tadryanom.me/?a=commitdiff_plain;h=e2fb4ca028c177c51a2b602660a3109c202a2914;p=AdrOS.git test: expand test suite with security unit tests and GDB scripted checks New test_security.c (19 tests): - user_range_ok: 11 tests covering NULL, zero-len, valid user addr, kernel addr rejection, boundary spanning, overflow, max user range - bitmap: 4 tests for set/unset/cross-byte/all-bits operations - eflags sanitization: 4 tests verifying IOPL clearing and IF forcing New gdb_checks.py: - Automated GDB script for QEMU+GDB integrity checks - Verifies heap magic, PMM bitmap frame 0, max_frames, VGA mapping - Usage: make test-gdb Updated Makefile: - test-host now runs both test_utils (28) and test_security (19) = 47 tests - Added test-gdb target for GDB scripted checks - Total: 47 host unit tests + 19 smoke checks = 66 automated tests --- diff --git a/Makefile b/Makefile index 0e922ba..f675777 100644 --- a/Makefile +++ b/Makefile @@ -200,6 +200,23 @@ test-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 + @echo "[TEST-HOST] Compiling tests/test_security.c..." + @gcc -m32 -Wall -Wextra -Werror -Iinclude -o build/host/test_security tests/test_security.c + @./build/host/test_security + +# ---- GDB Scripted Checks (requires QEMU + GDB) ---- + +test-gdb: $(KERNEL_NAME) iso + @echo "[TEST-GDB] Starting QEMU with GDB stub..." + @rm -f serial.log + @test -f disk.img || dd if=/dev/zero of=disk.img bs=1M count=4 2>/dev/null + @qemu-system-i386 -smp 4 -boot d -cdrom adros-$(ARCH).iso -m 128M -display none \ + -drive file=disk.img,if=ide,format=raw \ + -serial file:serial.log -monitor none -no-reboot -no-shutdown \ + -s -S & + @sleep 1 + @gdb -batch -nx -x tests/gdb_checks.py adros-$(ARCH).bin || true + @-pkill -f "qemu-system-i386.*-s -S" 2>/dev/null || true # ---- All Tests ---- @@ -228,4 +245,4 @@ $(BUILD_DIR)/%.o: $(SRC_DIR)/%.S clean: rm -rf build $(KERNEL_NAME) -.PHONY: all clean iso run cppcheck sparse analyzer check test test-1cpu test-host test-all scan-build mkinitrd-asan +.PHONY: all clean iso run cppcheck sparse analyzer check test test-1cpu test-host test-gdb test-all scan-build mkinitrd-asan diff --git a/tests/gdb_checks.py b/tests/gdb_checks.py new file mode 100644 index 0000000..5a94de5 --- /dev/null +++ b/tests/gdb_checks.py @@ -0,0 +1,140 @@ +""" +AdrOS GDB Scripted Integrity Checks + +Usage: + qemu-system-i386 -s -S ... & + gdb adros-x86.bin -x tests/gdb_checks.py + +Runs automated checks on kernel state after boot: +- Heap integrity (magic numbers, free list consistency) +- PMM bitmap sanity (frame 0 always used, kernel region used) +- VGA mapping present at 0xC00B8000 +""" + +import gdb +import sys + +class AdrOSCheck: + def __init__(self): + self.passed = 0 + self.failed = 0 + + def check(self, name, condition, detail=""): + if condition: + print(f" PASS {name}") + self.passed += 1 + else: + print(f" FAIL {name} {detail}") + self.failed += 1 + + def read_u32(self, addr): + """Read a uint32_t from a kernel address.""" + try: + val = gdb.parse_and_eval(f"*(uint32_t*){addr:#x}") + return int(val) & 0xFFFFFFFF + except gdb.error: + return None + + def read_u8(self, addr): + """Read a uint8_t from a kernel address.""" + try: + val = gdb.parse_and_eval(f"*(uint8_t*){addr:#x}") + return int(val) & 0xFF + except gdb.error: + return None + +def run_checks(): + c = AdrOSCheck() + + print("\n=========================================") + print(" AdrOS GDB Integrity Checks") + print("=========================================\n") + + # Connect to QEMU + try: + gdb.execute("target remote :1234", to_string=True) + except gdb.error as e: + print(f" ERROR: Cannot connect to QEMU: {e}") + return 1 + + # Load symbols + try: + gdb.execute("file adros-x86.bin", to_string=True) + except gdb.error: + pass + + # Set a breakpoint after init completes (on the idle loop) + # We'll break on uart_print of "[init]" to know userspace started + gdb.execute("break *process_init", to_string=True) + gdb.execute("continue", to_string=True) + + # Now we're at process_init — heap and PMM are initialized. + # Step past it to ensure scheduler is set up. + gdb.execute("finish", to_string=True) + + # ---- Heap Integrity ---- + try: + head_val = gdb.parse_and_eval("(uint32_t)head") + head_addr = int(head_val) & 0xFFFFFFFF + + c.check("Heap head is non-NULL", head_addr != 0) + c.check("Heap head is in heap range", + head_addr >= 0xD0000000 and head_addr < 0xD4000000, + f"head={head_addr:#x}") + + if head_addr != 0: + magic = c.read_u32(head_addr) + c.check("Heap head magic is 0xCAFEBABE", + magic == 0xCAFEBABE, + f"magic={magic:#x}" if magic else "read failed") + except gdb.error as e: + c.check("Heap head accessible", False, str(e)) + + # ---- PMM Bitmap ---- + try: + # Frame 0 should always be marked as used (never allocate phys addr 0) + bitmap_byte0 = gdb.parse_and_eval("memory_bitmap[0]") + byte0 = int(bitmap_byte0) & 0xFF + c.check("PMM frame 0 is used (bit 0 of bitmap[0])", + (byte0 & 1) == 1, + f"bitmap[0]={byte0:#x}") + + # max_frames should be > 0 + mf = gdb.parse_and_eval("max_frames") + max_frames = int(mf) + c.check("PMM max_frames > 0", max_frames > 0, f"max_frames={max_frames}") + + # total_memory should be > 0 + tm = gdb.parse_and_eval("total_memory") + total_mem = int(tm) + c.check("PMM total_memory > 0", total_mem > 0, f"total_memory={total_mem}") + except gdb.error as e: + c.check("PMM bitmap accessible", False, str(e)) + + # ---- VGA Mapping ---- + try: + # VGA buffer should be mapped at 0xC00B8000 + vga_val = c.read_u32(0xC00B8000) + c.check("VGA mapping at 0xC00B8000 is readable", + vga_val is not None, + "read failed") + except gdb.error as e: + c.check("VGA mapping accessible", False, str(e)) + + # ---- Summary ---- + total = c.passed + c.failed + print(f"\n {c.passed}/{total} passed, {c.failed} failed") + + if c.failed > 0: + print(" RESULT: FAIL\n") + else: + print(" RESULT: PASS\n") + + # Kill QEMU + gdb.execute("kill", to_string=True) + gdb.execute("quit", to_string=True) + + return 1 if c.failed > 0 else 0 + +# Auto-run when loaded +run_checks() diff --git a/tests/test_security.c b/tests/test_security.c new file mode 100644 index 0000000..cbcf876 --- /dev/null +++ b/tests/test_security.c @@ -0,0 +1,249 @@ +/* + * AdrOS Host-Side Unit Tests for Security-Critical Functions + * + * Tests: user_range_ok, bitmap operations, eflags sanitization logic + * + * Compile: gcc -m32 -Iinclude -o build/test_security tests/test_security.c + * Run: ./build/test_security + */ + +#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(" %-44s ", #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_TRUE(x) ASSERT_EQ(!!(x), 1) +#define ASSERT_FALSE(x) ASSERT_EQ(!!(x), 0) + +/* ---- Functions under test ---- */ + +/* From src/kernel/uaccess.c (weak default, with the fix applied) */ +#define USER_ADDR_LIMIT 0xC0000000U + +static 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 */ + if (uaddr >= USER_ADDR_LIMIT) return 0; /* kernel address */ + if (end >= USER_ADDR_LIMIT) return 0; /* spans into kernel */ + return 1; +} + +/* From src/mm/pmm.c — bitmap operations */ +static uint8_t test_bitmap[16]; /* 128 bits = 128 frames */ + +static void bitmap_set(uint64_t bit) { + test_bitmap[bit / 8] |= (uint8_t)(1 << (bit % 8)); +} + +static void bitmap_unset(uint64_t bit) { + test_bitmap[bit / 8] &= (uint8_t)~(1 << (bit % 8)); +} + +static int bitmap_test(uint64_t bit) { + return test_bitmap[bit / 8] & (1 << (bit % 8)); +} + +/* eflags sanitization logic from sigreturn fix */ +static uint32_t sanitize_eflags(uint32_t eflags) { + return (eflags & ~0x3000U) | 0x200U; +} + +/* ======== user_range_ok TESTS ======== */ + +TEST(urange_null_ptr) { + ASSERT_FALSE(user_range_ok(NULL, 10)); +} + +TEST(urange_zero_len) { + ASSERT_TRUE(user_range_ok(NULL, 0)); + ASSERT_TRUE(user_range_ok((void*)0x1000, 0)); +} + +TEST(urange_valid_user) { + ASSERT_TRUE(user_range_ok((void*)0x08048000, 4096)); +} + +TEST(urange_kernel_addr) { + ASSERT_FALSE(user_range_ok((void*)0xC0000000, 1)); +} + +TEST(urange_kernel_addr_high) { + ASSERT_FALSE(user_range_ok((void*)0xC0100000, 100)); +} + +TEST(urange_spans_boundary) { + /* Start in user space, end in kernel space */ + ASSERT_FALSE(user_range_ok((void*)0xBFFFF000, 0x2000)); +} + +TEST(urange_just_below_limit) { + ASSERT_TRUE(user_range_ok((void*)0xBFFFFFFF, 1)); +} + +TEST(urange_at_limit) { + ASSERT_FALSE(user_range_ok((void*)0xC0000000, 1)); +} + +TEST(urange_overflow) { + /* uaddr + len wraps around */ + ASSERT_FALSE(user_range_ok((void*)0xFFFFFFFF, 2)); +} + +TEST(urange_max_user) { + /* Largest valid user range: 0x1 to 0xBFFFFFFF */ + ASSERT_TRUE(user_range_ok((void*)0x1, 0xBFFFFFFF)); +} + +TEST(urange_max_user_plus_one) { + /* 0x1 + 0xC0000000 - 1 = 0xC0000000 which is kernel */ + ASSERT_FALSE(user_range_ok((void*)0x1, 0xC0000000)); +} + +/* ======== Bitmap TESTS ======== */ + +TEST(bitmap_set_and_test) { + memset(test_bitmap, 0, sizeof(test_bitmap)); + bitmap_set(0); + ASSERT_TRUE(bitmap_test(0)); + ASSERT_FALSE(bitmap_test(1)); +} + +TEST(bitmap_unset) { + memset(test_bitmap, 0xFF, sizeof(test_bitmap)); + bitmap_unset(7); + ASSERT_FALSE(bitmap_test(7)); + ASSERT_TRUE(bitmap_test(6)); + ASSERT_TRUE(bitmap_test(8)); +} + +TEST(bitmap_cross_byte) { + memset(test_bitmap, 0, sizeof(test_bitmap)); + bitmap_set(7); /* last bit of byte 0 */ + bitmap_set(8); /* first bit of byte 1 */ + ASSERT_TRUE(bitmap_test(7)); + ASSERT_TRUE(bitmap_test(8)); + ASSERT_FALSE(bitmap_test(6)); + ASSERT_FALSE(bitmap_test(9)); +} + +TEST(bitmap_all_bits) { + memset(test_bitmap, 0, sizeof(test_bitmap)); + for (int i = 0; i < 128; i++) { + bitmap_set((uint64_t)i); + } + for (int i = 0; i < 128; i++) { + ASSERT_TRUE(bitmap_test((uint64_t)i)); + } + /* Unset every other bit */ + for (int i = 0; i < 128; i += 2) { + bitmap_unset((uint64_t)i); + } + for (int i = 0; i < 128; i++) { + if (i % 2 == 0) { + ASSERT_FALSE(bitmap_test((uint64_t)i)); + } else { + ASSERT_TRUE(bitmap_test((uint64_t)i)); + } + } +} + +/* ======== eflags sanitization TESTS ======== */ + +TEST(eflags_clears_iopl) { + /* IOPL=3 (bits 12-13 set) should be cleared */ + uint32_t dirty = 0x3000 | 0x200; /* IOPL=3, IF=1 */ + uint32_t clean = sanitize_eflags(dirty); + ASSERT_EQ(clean & 0x3000, 0); /* IOPL cleared */ + ASSERT_TRUE(clean & 0x200); /* IF still set */ +} + +TEST(eflags_sets_if) { + /* IF=0 should be forced to IF=1 */ + uint32_t dirty = 0; /* no flags */ + uint32_t clean = sanitize_eflags(dirty); + ASSERT_TRUE(clean & 0x200); /* IF set */ +} + +TEST(eflags_preserves_other) { + /* CF=1 (bit 0), ZF=1 (bit 6), SF=1 (bit 7) should be preserved */ + uint32_t dirty = 0x3000 | 0x01 | 0x40 | 0x80; /* IOPL=3, CF, ZF, SF */ + uint32_t clean = sanitize_eflags(dirty); + ASSERT_TRUE(clean & 0x01); /* CF preserved */ + ASSERT_TRUE(clean & 0x40); /* ZF preserved */ + ASSERT_TRUE(clean & 0x80); /* SF preserved */ + ASSERT_EQ(clean & 0x3000, 0); /* IOPL cleared */ + ASSERT_TRUE(clean & 0x200); /* IF set */ +} + +TEST(eflags_iopl1) { + /* IOPL=1 should also be cleared */ + uint32_t dirty = 0x1000 | 0x200; + uint32_t clean = sanitize_eflags(dirty); + ASSERT_EQ(clean & 0x3000, 0); +} + +/* ======== MAIN ======== */ +int main(void) { + printf("\n=========================================\n"); + printf(" AdrOS Security Unit Tests\n"); + printf("=========================================\n\n"); + + /* user_range_ok */ + RUN(urange_null_ptr); + RUN(urange_zero_len); + RUN(urange_valid_user); + RUN(urange_kernel_addr); + RUN(urange_kernel_addr_high); + RUN(urange_spans_boundary); + RUN(urange_just_below_limit); + RUN(urange_at_limit); + RUN(urange_overflow); + RUN(urange_max_user); + RUN(urange_max_user_plus_one); + + /* bitmap */ + RUN(bitmap_set_and_test); + RUN(bitmap_unset); + RUN(bitmap_cross_byte); + RUN(bitmap_all_bits); + + /* eflags */ + RUN(eflags_clears_iopl); + RUN(eflags_sets_if); + RUN(eflags_preserves_other); + RUN(eflags_iopl1); + + 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; +}