]> Projects (at) Tadryanom (dot) Me - AdrOS.git/commitdiff
test: expand test suite with security unit tests and GDB scripted checks
authorTulio A M Mendes <[email protected]>
Tue, 10 Feb 2026 11:44:55 +0000 (08:44 -0300)
committerTulio A M Mendes <[email protected]>
Fri, 13 Feb 2026 02:20:50 +0000 (23:20 -0300)
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

Makefile
tests/gdb_checks.py [new file with mode: 0644]
tests/test_security.c [new file with mode: 0644]

index 0e922baedaab15ee7f686283c5a043f6dc3e3b94..f6757773e34cf60176b184e653c7adad5ae89886 100644 (file)
--- 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 (file)
index 0000000..5a94de5
--- /dev/null
@@ -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 (file)
index 0000000..cbcf876
--- /dev/null
@@ -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 <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("  %-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;
+}