M1 through M7 complete and green, plus two post-M7 gap-closure passes against Docs 1-4 (per-process address spaces, then VGA output/guard pages/fault-safe copies/register dumps/log levels/README+Makefile -- see DECISIONS.md for both addenda). M8 (stretch: ELF loader, ramdisk/filesystem, disk driver, SMP) is not started and is explicitly out of scope for this run; see DECISIONS.md.
cargo runboots to long mode via thebootloader0.9 crate, reacheskmaininsrc/main.rs, and prints a banner over COM1 serial and the VGA text buffer (src/vga.rs, Doc 2 section 3) (Flint kernel booting.../Flint vX.Y.Z -- boot OK).flint::init()loads the GDT/TSS (with the double-fault IST stack), loads the IDT, remaps and unmasks the PIC, and enables interrupts. The timer (IRQ0) increments a tick counter; the keyboard (IRQ1) decodes scancodes and echoes to serial.- Panic handler prints
KERNEL PANIC: <info>(plus a best-effort 4-register snapshot) over serial and VGA, then halts (hlt_loop), for the non-test build. Exception-specific panics (a kernel-mode page fault, a general protection fault, a double fault) carry a genuine, at-fault full register dump instead -- see the GPR-capture-trampoline note below. flint::init_memory(boot_info)walks the bootloader's memory map and builds the physical frame allocator (src/memory/frame.rs), an intrusive free list threaded through the free frames' own backing memory. Physical frame 0 is never handed out. Also brings up VGA (vga::init), which needs the same physical-memory offset mapping this call establishes.- The same call now also brings up the page table mapper (
src/memory/paging.rs, anOffsetPageTableover the bootloader's physical-memory mapping) and the kernel heap (src/memory/heap.rs, a fixed-size-block allocator with a linked-list fallback, installed as#[global_allocator]), soalloc(Box,Vec,BTreeMap) works from here on. Virtual page 0 can never be mapped --map_pageasserts on it -- so a null-pointer dereference always faults. - The kernel-mode page-fault handler demand-pages a not-present fault inside a designated "lazy" virtual region (
memory::paging::LAZY_REGION_*), mapping a fresh frame and letting the faulting instruction re-run; redirects execution instead (see the fault-safe-copy note below) if the fault happened inside a syscall's user-pointer copy; anything else (a protection violation, or a not-present fault outside those two cases) panics with a full register dump, per Doc 3 section 5 and Doc 4 section 5. - The PIT (IRQ0's source) is reprogrammed from its ~18.2 Hz BIOS default to 100 Hz (
interrupts::init_pit), giving the scheduler a known 10ms quantum. flint::init_scheduler()starts a preemptive round-robin scheduler (src/task/scheduler.rs) with a placeholder task standing in for the boot thread.task::scheduler::spawnadds kernel threads to the ready queue; the timer handler callsscheduler::timer_tick()(after sending the PIC its EOI, since a switch may not return for a long time) to pop the next ready task and hand-written asm (src/task/context.rs,#[unsafe(naked)]) to actually switch stacks. All kernel threads share the kernel's one address space -- ring-3 processes get their own (see the M6 addendum below); a kernel thread's stack is now a mapped region with an unmapped guard page immediately below it (task::map_task_stack), not a plain heapBox, per Doc 3 section 3.task::scheduler::current_task_id()and a smalllog_trace!/log_debug!/log_info!/log_warn!/log_error!macro set (src/serial.rs) give every diagnostic line a level and, once the scheduler exists, a[task N]tag, per Doc 4 section 2 -- replacing the ad hoc[user]/[syscall]/[shell]prefixes the original M1-M7 build used.flint::user::setup()maps an isolated user code page (read-only + executable after the demo program's bytes are copied in) and an isolated user stack page (writable +NO_EXECUTE), each in its own separate top-level (PML4) page-table region so neither mapping can ever share -- and therefore silently weaken -- the other's page-table entries at any level.flint::user::enter_user_modebuilds an IRETQ frame by hand and drops to ring 3.- The syscall path (
src/syscall/mod.rs) usesint 0x80(IDT vector 0x80, gate DPL set to Ring3) with a hand-written naked entry stub (syscall_entry) that savesrax/rdi/rsi, dispatches to normal Rust, andiretqs back.SYS_WRITEandSYS_EXITare implemented. Every user pointer is validated -- range, mapped, user-accessible, via a real page-table walk (Translate), not a hardcoded address-range check -- before it is ever dereferenced; a bad pointer returns an error code and logs why, and the kernel keeps running. The copy itself (not just the earlier validation) is now also fault-safe:copy_from_user_byte/copy_to_user_byterecord a recovery point the page-fault handler redirects to instead of panicking if a validated mapping gets revoked before the copy runs (Doc 3 sections 4/7) -- see DECISIONS.md's gap-closure addendum. src/user/program.rsis Flint's first user program: hand-written position-independent machine code (no ELF loader yet -- M8 stretch) that issues a validSYS_WRITE, aSYS_WRITEwith a pointer into the kernel heap (mapped, but never user-accessible), thenSYS_EXIT.- M6 addendum (post-M7): per-process address spaces. Each ring-3 process now gets its own PML4 (
memory::paging::new_address_space), cloned from the currently-active table so already-populated kernel mappings (kernel image, the physical-memory-offset mapping, the heap) stay identically reachable, while the process's own code/stack/buffer pages are mapped only into its own table and are invisible to any other.memory::paging::activateswitches CR3 to it and rebuilds the kernel's single cached page-table mapper to match, so the existingmap_page/validate_user_range/demand-paging call sites transparently target whichever address space is active with no call-site changes. Kernel threads (M5) are unaffected -- they still share the kernel's one CR3, sincecontext::switchnever touches CR3 and ring-3 processes are not schedulerTasks (see DECISIONS.md's M6 addendum for the full reasoning, including the accepted "no fork/exec, one process per boot" scope assumption). What M6 delivers and tests, updated: privilege separation (ring 0 vs ring 3 enforced by the GDT/IRETQ), the syscall trust boundary (validated user pointers, hostile pointers rejected), W xor X for the user program's own two pages, and address-space isolation (a real, private top-level page table per process, not a shared one). src/shell/mod.rs: the command set (help,echo,meminfo,ps,ticks/uptime), a puredispatch(line: &str) -> Stringfunction independent of I/O, so it is directly#[test_case]-testable.meminforeports live frame-allocator stats;psreports the scheduler's context-switch count;ticks/uptimereportsinterrupts::ticks().- Two new syscalls close the shell's I/O loop:
SYS_READ_LINE(blocks on the UART, echoing each byte back as typed, validates the destination buffer as a writable user range -- the copy-to-user half of the checked-copy pattern, whichSYS_WRITEalone never exercised) andSYS_SHELL_DISPATCH(validates and reads the line, callsshell::dispatch, prints the response).src/user/shell_program.rsis the ring-3 process: a hand-written asm loop of read -> dispatch -> repeat, ending onexit/quit.cargo runnow boots straight into this shell. - M7 scope note: the shell process's control flow (the loop, the syscalls it issues, ending on
exit) genuinely runs in ring 3, per Doc 2 section 8 ("a user-space process, not a kernel feature"). Command parsing runs in the kernel behindSYS_SHELL_DISPATCH, not in ring-3 code, because Flint has no ELF loader (M8 stretch) and hand-encoding a full string-matching parser directly in raw assembly was judged not worth the added risk this late in the build. See DECISIONS.md.
cargo test --lib:flint::trivial_assertion,flint::interrupts::tests::test_breakpoint_exception,flint::memory::tests::allocated_frames_are_distinct_and_nonzero,flint::memory::tests::freed_frame_is_reused,flint::memory::heap::tests::{boxed_value_round_trips, large_vec_uses_every_slot, many_boxes_dont_exhaust_the_heap, large_allocation_uses_fallback_path},flint::memory::paging::tests::page_fault_on_lazy_region_is_handled_and_continues(the PRD's "valid-but-unmapped page fault is handled and execution continues" gate: writes through an intentionally never-pre-mapped pointer, then reads the value back to prove a real frame landed there, not just that the fault was swallowed),flint::vga::tests::printed_text_round_trips_through_the_buffer(writes a known string to VGA, reads the exact cells back out of the buffer),flint::syscall::tests::copy_helpers_return_err_instead_of_panicking_on_a_mid_copy_fault(Doc 3 sections 4/7's fault-safe-copy gate: validates a range, deliberately unmaps it mid-sequence, and asserts the copy helpers returnErr-- reaching the assertions at all, rather than a kernel panic, is itself the proof).cargo test --test basic_boot:basic_boot::test_boots_and_prints-- boots and prints.cargo test --test stack_overflow(harness = false): double-fault-on-stack-overflow gate (Doc 3 section 5/7), on the boot thread's own stack.cargo test --test task_stack_overflow(harness = false, new): the same double-fault-on-overflow gate, but for a kernel task's mapped-plus-guard-page stack (Doc 3 section 3) rather than the boot stack -- spawns a self-recursing task and confirms the real kernel double-fault handler catches it (needs the timer/scheduler running to reach the task at all, so it reuses the real IDT rather than a test-local one likestack_overflow.rs).cargo test --test null_page(harness = false, new): dereferences virtual address 0 in kernel mode and asserts the fault is a genuine not-present fault on page 0 caught by a test-local handler, not a crash and not a lucky read of real memory -- the Doc 3 section 3/7 "page 0 is unmapped" gate.cargo test --test register_dump(harness = false, new): Doc 4 section 5's register-dump gate. Loads a known marker value intorax, triggers a genuine, unrecoverable kernel-mode page fault on a deliberately unmapped canonical address, and asserts the panic report'srax=field contains the exact marker -- direct proof the GPR-capture trampolines reflect real register state, not zeros or a stub.flint::task::scheduler::tests::two_tasks_alternate_under_the_timer: the PRD's "two tasks alternate under the timer a known number of times" gate. Spawns two kernel threads, each an infinite loop with no voluntary yield incrementing its own atomic counter; waits until both reach 20. Since neither task can make progress without genuine preemption, and 40+ actual context switches are asserted to have happened, this is direct proof the scheduler and the hand-written context switch both work, not just that the scheduler ran once.cargo test --test user_mode(harness = false): the full M6 demo end to end. Boots, sets up ring 3, jumps to the hand-written user program, and hooksSYS_EXITto report success and exit QEMU instead of its normalhlt_loop. Serial transcript on a green run:[info] [task 0] private address space: PML4 PhysFrame[4KiB](0x7edc000) (boot PML4 was PhysFrame[4KiB](0x1000))(direct evidence the process got its own top-level page table, distinct from the boot one -- the PRD's FR-MEM-2/FR-USER-1 gate),[user] hi ring3!(the valid syscall's effect, visible over serial -- the PRD's "user-mode program performs a syscall and returns" gate),[warn] [task 0] SYS_WRITE rejected: ...(the hostile pointer into the kernel heap, rejected -- the "hostile user pointer is rejected and the kernel survives" gate, proven by the fact that execution continues past it),[info] [task 0] exited via SYS_EXIT, then the test's own[ok]. Any fault along the way (a bad IRETQ frame, a wrong syscall offset) surfaces as a kernel panic this test's own panic handler reports as failed, not a silent hang.flint::shell::tests::*(new):help_lists_commands,echo_returns_its_argument,echo_with_no_argument_is_empty,meminfo_reports_frame_counts,ps_reports_a_status_line,ticks_and_uptime_are_synonyms,unknown_command_says_so,blank_line_is_a_no_op-- the command-dispatch logic tested directly, independent of the ring-3/syscall path.cargo test --test shell(harness = false, new): the M7 gate, "the shell echoes input and runs help and one status command." Boots straight into the ring-3 shell loop withSYS_READ_LINEfed a scripted byte sequence (help,meminfo,exit) viasyscall::set_scripted_input, so the test is self-contained and deterministic under a barecargo testrather than depending on external stdin reaching this exact QEMU process. Green transcript:helpechoed and answered with the command list,meminfoechoed and answered with live frame stats,exitends the ring-3 loop via its ownSYS_EXIT, hooked to report success.- The scripted path exercises the identical ring-3/syscall/validate/echo pipeline real typed input uses -- only the byte source differs. Real interactive input was also manually verified:
printf "\nhelp\nps\nticks\nexit\n" | qemu-system-x86_64 -drive format=raw,file=target/x86_64-flint/debug/bootimage-flint.bin -device isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio -display none -no-rebootproduces the same live back-and-forth over real serial I/O, confirmed by hand during this build (piping stdin throughcargo run's own process chain also reaches the UART, but is not used as the automated harness test for the reason above -- see DECISIONS.md). The leading blank line in that command is a warm-up byte for a benign UART startup race (the first byte typed immediately at boot can arrive before the kernel starts polling the UART and get overwritten before it's read); also documented in DECISIONS.md.
- The scripted path exercises the identical ring-3/syscall/validate/echo pipeline real typed input uses -- only the byte source differs. Real interactive input was also manually verified:
- Bare
cargo testruns all of the above plus doctests (0, sinceno_std) in one invocation, all green. - QEMU is run headless (
-display none -serial stdio) so serial is what the harness (and a human) actually observes.
- Nothing required remains: M1 through M7, plus both post-M7 gap-closure addenda, are all green. M8 (ELF loader, ramdisk/filesystem, disk driver, SMP) is stretch and untouched -- see SUMMARY.md for the honest per-requirement status and DECISIONS.md for why each M8 item was left as a documented stub rather than attempted.
See README.md for the four commands (build/run/test/debug) and their exact form; make debug now wraps what used to be a hand-typed gdb-stub QEMU invocation. One command not in that list, for manually driving a live interactive session over real serial I/O (not the scripted-input test path, and not cargo run's own headless invocation): after cargo build, qemu-system-x86_64 -drive format=raw,file=target/x86_64-flint/debug/bootimage-flint.bin -device isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio -display none -no-reboot.