Skip to content
Closed
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
65 changes: 65 additions & 0 deletions .github/workflows/kernel-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Kernel Unit Tests

on:
push:
branches:
- "**"
pull_request:

jobs:
dynpriv-lint:
name: dynpriv lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Run dynamic privilege lint
run: bash scripts/dynpriv-lint.sh

unit-tests:
name: unit-tests (${{ matrix.arch }})
runs-on: ubuntu-latest
needs: dynpriv-lint
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
arch: [x86_64, aarch64]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install build and QEMU dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
clang lld llvm \
qemu-system-x86 qemu-system-arm \
ovmf qemu-efi-aarch64 \
mtools gdisk xorriso \
gdb-multiarch

- name: Fetch Limine EFI binaries
run: make limine

- name: Run unit tests
shell: bash
run: |
set -euo pipefail
if [[ "${{ matrix.arch }}" == "aarch64" ]]; then
timeout_sec=240
else
timeout_sec=180
fi

make test ARCH=${{ matrix.arch }} UNIT_TEST_TIMEOUT=${timeout_sec} \
| tee unit-tests-${{ matrix.arch }}.log

- name: Upload test log
if: always()
uses: actions/upload-artifact@v4
with:
name: unit-tests-${{ matrix.arch }}-log
path: unit-tests-${{ matrix.arch }}.log
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ build/
# Generated images
images/*.img
images/*.iso
images/unit-tests-*/

# Downloaded Limine binaries (reproducible via make limine)
boot/limine/*.EFI
Expand All @@ -18,6 +19,9 @@ boot/limine/limine
*.swp
*~

# Python cache
scripts/__pycache__/

# Clangd
.cache/
compile_commands.json
Expand Down
42 changes: 42 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# AGENTS.md

## Onboarding

Before starting any task, new agents should perform the equivalent of the `/gather-context` command (see `.cursor/commands/gather-context.md`). In short:

1. **Read project rules** (in order): `.cursor/rules/philosophy.md`, `architecture.md`, `style.md`, `low-level.md`, `error-handling.md`, `build.md`, `cpp-constraints.md`
2. **Explore directory structure**: `kernel/common/` (arch-independent interfaces), `kernel/arch/{x86_64,aarch64}/` (implementations), `kernel/boot/`, `kernel/mm/`, `kernel/trap/`, `kernel/sched/`, `kernel/syscall/`
3. **Read key source files**: a common header + both arch implementations, `start.S` and `arch_init` for both architectures, `Makefile` and `kernel/Makefile`
4. **Check current state**: `git log --oneline -20` to see recent development focus, and scan for TODOs/FIXMEs

This context is essential — the project has strict dual-architecture requirements, a freestanding C++20 subset (no STL), and specific coding conventions that are easy to violate without reading the rules first.

## Cloud-specific instructions

### Overview

Stellux 3.0 is a bare-metal hobby OS kernel written in freestanding C++20 and assembly, targeting **x86_64** and **AArch64**. There is no web server, database, or container infrastructure. The "application" is the kernel itself, tested by booting in QEMU and inspecting serial output.

### Prerequisites (system packages)

System dependencies are installed via `make deps` (wraps `sudo apt install`). The Limine UEFI bootloader binaries are fetched via `make limine` (downloads from GitHub). Both are one-time setup steps already handled by the update script.

### Build & Run commands

See `Makefile` help (`make help`) and `.cursor/rules/build.md` for the full command reference. Key commands:

- **Build**: `make kernel ARCH=x86_64` / `make kernel ARCH=aarch64`
- **Disk image**: `make image ARCH=x86_64` / `make image ARCH=aarch64`
- **Run with display**: `make run ARCH=x86_64`
- **Run headless** (for cloud/CI): `make run-headless ARCH=x86_64`
- **Lint**: `bash scripts/dynpriv-lint.sh`
- **Toolchain check**: `make toolchain-check`

### Gotchas for cloud environments

- **Headless mode required**: Use `make run-headless ARCH=<arch>` since there is no display server. QEMU uses `-nographic` and serial output goes to stdout.
- **OVMF_VARS.fd is mutable**: The x86_64 QEMU target requires a writable copy of the OVMF VARS file at `build/OVMF_VARS.fd`. This is auto-created by `make run*` targets, but if `build/` was cleaned you need to rebuild via `make image ARCH=x86_64` first.
- **AArch64 QEMU is slower**: AArch64 runs under TCG emulation (no KVM for cross-arch), so QEMU boot takes ~20-40s. Use `timeout 45` when scripting.
- **No automated test suite**: There is no unit test framework. Verification is done by booting the kernel in QEMU and checking serial output for `[INFO] Stellux 3.0 booting...` through `Initialization complete! Halting...`.
- **Both architectures must build and pass**: A feature is not complete until both `ARCH=x86_64` and `ARCH=aarch64` build without errors and boot successfully.
- **Linter**: `scripts/dynpriv-lint.sh` checks dynamic privilege annotation consistency (header vs source `__PRIVILEGED_CODE` markers and docstrings). Run it before committing changes to kernel code.
52 changes: 51 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ GDB_PORT := 4554
# QEMU settings
QEMU_MEMORY := 4G

# Unit test defaults (also defined in config.mk for kernel builds)
UNIT_TEST ?= 0
UNIT_TEST_FILTER ?=
UNIT_TEST_FAIL_FAST ?= 0
UNIT_TEST_REPEAT ?= 1
UNIT_TEST_SEED ?= 0xC0FFEE
UNIT_TEST_TIMEOUT ?= 120

# Verbosity (V=1 for verbose)
ifeq ($(V),1)
Q :=
Expand All @@ -55,6 +63,12 @@ export V
export DEBUG
export RELEASE
export BUILD_DIR
export UNIT_TEST
export UNIT_TEST_FILTER
export UNIT_TEST_FAIL_FAST
export UNIT_TEST_REPEAT
export UNIT_TEST_SEED
export UNIT_TEST_TIMEOUT

# ============================================================================
# Supported Architectures
Expand All @@ -67,7 +81,7 @@ SUPPORTED_ARCHS := x86_64 aarch64
# ============================================================================

# Targets that require ARCH
ARCH_REQUIRED_TARGETS := kernel image run
ARCH_REQUIRED_TARGETS := kernel image run run-headless test

# Check if current target requires ARCH
CURRENT_GOALS := $(MAKECMDGOALS)
Expand Down Expand Up @@ -100,6 +114,7 @@ endif
# ============================================================================

.PHONY: all kernel image run run-headless clean \
test test-all \
image-x86_64 image-aarch64 \
run-qemu-x86_64 run-qemu-aarch64 \
run-qemu-x86_64-headless run-qemu-aarch64-headless \
Expand Down Expand Up @@ -181,6 +196,33 @@ else ifeq ($(ARCH),aarch64)
$(Q)$(MAKE) run-qemu-aarch64-headless
endif

# ============================================================================
# Unit Test Execution
# ============================================================================

test: check-lld
python3 scripts/run-unit-tests.py \
--arch $(ARCH) \
--filter "$(UNIT_TEST_FILTER)" \
--fail-fast $(UNIT_TEST_FAIL_FAST) \
--repeat $(UNIT_TEST_REPEAT) \
--seed $(UNIT_TEST_SEED) \
--timeout $(UNIT_TEST_TIMEOUT)

test-all:
$(Q)$(MAKE) test ARCH=x86_64 \
UNIT_TEST_FILTER="$(UNIT_TEST_FILTER)" \
UNIT_TEST_FAIL_FAST=$(UNIT_TEST_FAIL_FAST) \
UNIT_TEST_REPEAT=$(UNIT_TEST_REPEAT) \
UNIT_TEST_SEED=$(UNIT_TEST_SEED) \
UNIT_TEST_TIMEOUT=$(UNIT_TEST_TIMEOUT)
$(Q)$(MAKE) test ARCH=aarch64 \
UNIT_TEST_FILTER="$(UNIT_TEST_FILTER)" \
UNIT_TEST_FAIL_FAST=$(UNIT_TEST_FAIL_FAST) \
UNIT_TEST_REPEAT=$(UNIT_TEST_REPEAT) \
UNIT_TEST_SEED=$(UNIT_TEST_SEED) \
UNIT_TEST_TIMEOUT=$(UNIT_TEST_TIMEOUT)

run-qemu-x86_64: $(IMAGE_DIR)/stellux-x86_64.img $(BUILD_DIR)/OVMF_VARS.fd
@echo ""
@echo "Serial output below. QEMU monitor: Ctrl+A C | Exit: Ctrl+A X"
Expand Down Expand Up @@ -488,6 +530,8 @@ help:
@echo " make image-aarch64 Build AArch64 disk image (shortcut)"
@echo " make run ARCH=<arch> Build + run in QEMU (with display)"
@echo " make run-headless ARCH=<arch> Build + run headless (for SSH)"
@echo " make test ARCH=<arch> Build + run in-kernel unit tests"
@echo " make test-all Run unit tests for both architectures"
@echo " make usb ARCH=<arch> Build + print USB instructions"
@echo ""
@echo "Debugging (run QEMU target in one terminal, connect-gdb in another):"
Expand All @@ -502,12 +546,18 @@ help:
@echo " V=1 Verbose output (show commands)"
@echo " DEBUG=1 Debug build (default)"
@echo " RELEASE=1 Release build (-O2)"
@echo " UNIT_TEST_FILTER=<expr> Test filter (suite prefix or suite.case)"
@echo " UNIT_TEST_FAIL_FAST=1 Stop after first failing test"
@echo " UNIT_TEST_REPEAT=<n> Repeat each matching test case"
@echo " UNIT_TEST_SEED=<u64> Deterministic test seed"
@echo " UNIT_TEST_TIMEOUT=<sec> Host timeout for make test"
@echo ""
@echo "Architectures: $(SUPPORTED_ARCHS)"
@echo ""
@echo "Examples:"
@echo " make kernel ARCH=x86_64"
@echo " make run ARCH=aarch64"
@echo " make test ARCH=x86_64"
@echo " make kernel ARCH=x86_64 RELEASE=1 V=1"
@echo ""
@echo "Other:"
Expand Down
27 changes: 27 additions & 0 deletions config.mk
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,30 @@ MAX_CPUS ?= 64

# Log level (0=debug, 1=info, 2=warn, 3=error, 4=fatal, 5=none)
LOG_LEVEL ?= 0

# ============================================================================
# Unit Test Configuration
# ============================================================================

# Build with in-kernel unit test framework enabled.
# 0 = disabled (default), 1 = enabled.
UNIT_TEST ?= 0

# Optional test filter:
# "" => run all tests
# "suite_prefix" => run suites matching prefix
# "suite_name.case_name" => run one test case
UNIT_TEST_FILTER ?=

# Stop execution immediately after the first failing test case.
# 0 = continue running all tests (default), 1 = fail-fast.
UNIT_TEST_FAIL_FAST ?= 0

# Repeat each matching test case this many times (>= 1).
UNIT_TEST_REPEAT ?= 1

# Seed for deterministic per-test pseudo-random generation.
UNIT_TEST_SEED ?= 0xC0FFEE

# Host-side unit test timeout (seconds) for QEMU runner.
UNIT_TEST_TIMEOUT ?= 120
16 changes: 16 additions & 0 deletions kernel/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ CXX_SOURCES += $(shell find percpu -name '*.cpp' 2>/dev/null | sort)
CXX_SOURCES += $(shell find syscall -name '*.cpp' 2>/dev/null | sort)
CXX_SOURCES += $(shell find arch/$(ARCH) -name '*.cpp' 2>/dev/null | sort)

ifeq ($(UNIT_TEST),1)
CXX_SOURCES += $(shell find test/framework -name '*.cpp' 2>/dev/null | sort)
CXX_SOURCES += $(shell find test/common -name '*.cpp' 2>/dev/null | sort)
CXX_SOURCES += $(shell find test/arch/$(ARCH) -name '*.cpp' 2>/dev/null | sort)
endif

# Find all assembly sources (arch-specific only typically)
ASM_SOURCES := $(shell find arch/$(ARCH) -name '*.S' 2>/dev/null | sort)

Expand Down Expand Up @@ -109,6 +115,14 @@ CXXFLAGS_LOG := -DLOG_LEVEL=$(LOG_LEVEL)
# Configuration defines (from config.mk)
CXXFLAGS_CONFIG := -DMAX_CPUS=$(MAX_CPUS)

# Unit test config flags (used by framework and boot flow)
CXXFLAGS_TEST := \
-DUNIT_TEST=$(UNIT_TEST) \
-DUNIT_TEST_FAIL_FAST=$(UNIT_TEST_FAIL_FAST) \
-DUNIT_TEST_REPEAT=$(UNIT_TEST_REPEAT) \
-DUNIT_TEST_SEED=$(UNIT_TEST_SEED) \
-DUNIT_TEST_FILTER=\"$(UNIT_TEST_FILTER)\"

# Include paths
# Note: -Iarch/$(ARCH) allows arch-specific headers to be included with paths
# like #include "hw/barrier.h" - the build system picks the right arch directory
Expand All @@ -124,6 +138,7 @@ CXXFLAGS := \
$(CXXFLAGS_MODE) \
$(CXXFLAGS_LOG) \
$(CXXFLAGS_CONFIG) \
$(CXXFLAGS_TEST) \
$(CXXFLAGS_INCLUDES) \
$(CXXFLAGS_EXTRA)

Expand Down Expand Up @@ -159,6 +174,7 @@ ifeq ($(V),1)
@echo "CXXFLAGS=$(CXXFLAGS)"
@echo "ASFLAGS=$(ASFLAGS)"
@echo "LDFLAGS=$(LDFLAGS)"
@echo "UNIT_TEST=$(UNIT_TEST)"
@echo "SOURCES=$(CXX_SOURCES) $(ASM_SOURCES)"
@echo ""
else
Expand Down
15 changes: 15 additions & 0 deletions kernel/arch/aarch64/linker.ld
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,25 @@ SECTIONS {
.rodata : AT(ADDR(.rodata) - KERNEL_VADDR) {
__rodata_start = .;
*(.rodata .rodata.*)

/* Unit test registration tables (kept/sorted for deterministic order) */
. = ALIGN(8);
__stlx_test_suites_start = .;
KEEP(*(SORT_BY_NAME(.stlx_test_suites)))
KEEP(*(SORT_BY_NAME(.stlx_test_suites.*)))
__stlx_test_suites_end = .;

. = ALIGN(8);
__stlx_test_cases_start = .;
KEEP(*(SORT_BY_NAME(.stlx_test_cases)))
KEEP(*(SORT_BY_NAME(.stlx_test_cases.*)))
__stlx_test_cases_end = .;

/* Privileged rodata */
. = ALIGN(4096);
__priv_rodata_start = .;
*(.priv.rodata .priv.rodata.*)
. = ALIGN(4096);
__rodata_end = .;
} :rodata

Expand Down
15 changes: 15 additions & 0 deletions kernel/arch/x86_64/linker.ld
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,25 @@ SECTIONS {
.rodata : AT(ADDR(.rodata) - KERNEL_VADDR) {
__rodata_start = .;
*(.rodata .rodata.*)

/* Unit test registration tables (kept/sorted for deterministic order) */
. = ALIGN(8);
__stlx_test_suites_start = .;
KEEP(*(SORT_BY_NAME(.stlx_test_suites)))
KEEP(*(SORT_BY_NAME(.stlx_test_suites.*)))
__stlx_test_suites_end = .;

. = ALIGN(8);
__stlx_test_cases_start = .;
KEEP(*(SORT_BY_NAME(.stlx_test_cases)))
KEEP(*(SORT_BY_NAME(.stlx_test_cases.*)))
__stlx_test_cases_end = .;

/* Privileged rodata */
. = ALIGN(4096);
__priv_rodata_start = .;
*(.priv.rodata .priv.rodata.*)
. = ALIGN(4096);
__rodata_end = .;
} :rodata

Expand Down
Loading