Changelog

Reverse-chronological log of shipped work. Dates are merge/commit dates; PR numbers link to the merge that shipped each milestone. Sections under each release: Added / Changed / Fixed / Removed.

For what’s queued up next, see the roadmap; this file is the trail of how the current state got there.

0.9.0 — Kernel self-host with TCC (May 2026)

Version bump: 0.8.1 → 0.9.0. The bootable Multiboot 2 kernel ELF is now built end-to-end with our shipped TCC — host-side via ./build-kernel-tcc.sh, in-OS via the generated /apps/rebuild-kernel.sh (kernel-sh script driving /apps/tcc.elf once per source). Boot banner reports build origin (gcc-host / tcc-host / tcc-in-os). See the kernel rebuild guide.

Added

  • build-kernel-tcc.sh host driver + generator for the in-OS rebuild script. Single source of truth (one file list).
  • src/userspace/{rebuild-kernel.sh,kend.S} staged at /apps/{rebuild-kernel.sh,kend.S} on every ISO.
  • kernel/atomic.h — TCC-only shims for __atomic_* builtins and __builtin_unreachable, using lock-prefixed asm.
  • mb2_header_check.c — static layout assertion guarding boot.S’s hardcoded MB2_HEADER_LEN = 48.
  • arch/i386/boot/build_origin.c — three-way build-origin marker.
  • ISO staging: /usr/include/kernel-build/ (re-exposed kernel headers), /usr/lib/tcc/include/{stdint,limits}.h (Makar TCC stubs).
  • Opt-in test-mode phase REBUILD-KERNEL via TEST_CMDLINE='test_mode test=rebuild-kernel'.

Changed

  • kmalloc rounds size up to 4-byte alignment — root cause of a long-tail heap corruption under heavy fork/exec churn (every odd- sized allocation misaligned the next remainder block; the cascade eventually poisoned the freelist).
  • kfree range-checks ptr and validates blk->next lies in-heap inside the coalesce loop. fd_close skips kfree on out-of-range or unaligned e->data. Both emit a one-line diagnostic naming the caller via __builtin_return_address if anything else ever produces a bogus pointer.
  • boot.S / chainload.S / isr_asm.S: #ifdef __TINYC__ paths hardcode forward symbol offsets and rewrite GAS .macro as cpp #define. GCC build untouched.
  • vendor/tinycc/tccasm.c: NULL-guard asm_expr_sum so forward symbol references error cleanly instead of segfaulting TCC.
  • syscall.c: hoisted SYS_READDIR’s nested callback to file scope (TCC rejects gcc nested functions).
  • libc/stdlib/abort.c: drop __builtin_unreachable() under TCC.
  • Boot banner now distinguishes which compiler/where built the kernel: Host-built kernel! (GCC), Self-hosted kernel! (TCC, host build), or Self-hosted and built inside Makar! (TCC).
  • task.c reaper: fixed misplaced t->script_vars = NULL that was inside the if (t->exec_params) block (latent leak / UAF cousin of the kmalloc-alignment bug).
  • Shell path-style dispatch (/abs/foo or ./foo) now peeks the file’s first 4 bytes for the ELF magic instead of relying on a .sh suffix — bash-flavoured auto-detect: ELF → exec, anything else → script interpreter.

Open (handed off)

  • In-OS rebuild script runs end-to-end with correct PASS/FAIL marker discipline, but ~19/75 per-file compiles still fail with (null):3811692: invalid number syntax — TCC’s own BufferedFile state corruption inside tcc.elf, not a kernel exec/argv bug. Tracked separately; host-side TCC build is unaffected.

0.8.x — In-OS TCC milestone (May 2026)

Version bump: 0.7.x → 0.8.x. tcc.elf (TinyCC v0.9.27 cross-built against the Makar libc shim) ships on every ISO at /apps/tcc.elf. The sysroot (/usr/{include,lib} plus /usr/lib/tcc/) is auto-staged by build-tcc.sh.

Added

  • /apps/tcc.elf self-rebuilds calc.elf, sh.elf, makbox.elf, hello.elf in-OS (verified by test_tcc_rebuild_*).
  • SYSCALL_FILE_MAX lifted to 16 MiB; kernel heap to 32 MiB to absorb TCC’s working set.
  • Ring-3 page faults + GPFs now deliver SIGSEGV via task_exit rather than panicking the kernel; panic screen names the running task on ring-0 faults.
  • Real VFS mount table (s_mounts[] in vfs.c); root=/dev/hdaN Multiboot 2 cmdline; auto-detect ext2 → FAT32 → CD-ROM emergency. /dev, /proc, /tmp, /log, /mnt/cdrom, /mnt/<name> all first-class table entries. Legacy /hd / /mnt/hd aliases retired.
  • /apps/sh.elf ring-3 shell MVP with inline editing, history, variables, $? expansion, fork+execve+wait4 dispatch. Coexists with the in-kernel shell.

Fixed

  • Upstream TCC NULL-deref in vendor/tinycc/tccelf.c:fill_local_got_entries triggered by every Makar app today (no GOT relocations).
  • USER_STACK_PAGES = 8 (from 1) — sh.c’s recursive-descent parser was overflowing the single 4 KiB user stack page.

0.7.0 — POSIX process model (PR #166)

Version bump: 0.6.0 → 0.7.0. fork+execve+wait4 is a significant capability layer — the rest of the userland roadmap (musl, dash, pipes, userland shell) now has the kernel-side foundation it needs.

Added — fork() + execve() + wait4() (slices 15-16)

POSIX process model on Makar. Detailed implementation walkthrough in internals §11; overview here.

  • PMM frame refcounts (slice 15a). uint8_t refcount[PMM_MAX_FRAMES] parallel to the bitmap. pmm_alloc_frame sets refcount=1; pmm_free_frame decrements and only releases the bitmap bit when the count hits zero; new pmm_inc_ref / pmm_ref_count. All pre-existing single-owner callers (heap, vmm) keep their behaviour for free.
  • vmm_clone_pd_cow() (slice 15b) — walks parent PD, marks user PTEs RO + software COW bit (VMM_PTE_COW, PTE bit 9), pmm_inc_ref each frame, mirrors into the child PD. Reloads CR3 if the parent is active.
  • COW #PF handler (slice 15c) in arch/i386/debug/debug.c — write fault on COW-tagged user PTE: refcount==1 fast path (clear COW + set RW), else alloc fresh frame + memcpy 4 KiB + dec old refcount + install fresh frame as RW. Falls through to existing panic for real faults. CR0.WP enabled at paging_init so kernel writes also fault on RO user pages.
  • SYS_FORK (= 2) + fork_child_iret (slice 15d). task_fork() in arch/i386/proc/task.c clones a task slot, deep-copies the fd_table via new fd_table_clone, inherits cwd/tty/user_brk, clones the PD via 15b, resets sig handlers. Child’s kernel stack hand-built so its first task_switch ret lands in fork_child_iret (in task_asm.S, a 1:1 mirror of the isr_common_stub epilogue) and iret’s back to ring 3 at exactly the same EIP where the parent’s int 0x80 returns, with EAX=0.
  • Multi-page COW + exit-code coverage (slice 15e). forktest.elf exercises five independent BSS sentinels (one single-page + four 4 KiB-aligned), child writes all of them after fork (independent COW faults), parent verifies all five remain at original values; child exits with status=42.
  • SYS_EXECVE (= 11) (slice 16a) — replaces caller’s address space with a new ELF. elf_exec extended to free the OLD user PD on success. Syscall handler copies path + argv into kernel scratch before the PD swap. POSIX-compliant signal handler reset on execve. execvetest.elf does fork → child-execve hello.elf → parent-survives.
  • SYS_WAIT4 (= 114) + TASK_ZOMBIE (slice 16b) — new lifecycle state between RUNNING and DEAD; child holds its slot + exit_status for the parent to read. task_t.parent_pid + task_t.exit_status added. task_exit decides ZOMBIE vs DEAD based on whether parent is a live ring-3 task (kernel-internal tasks go straight to DEAD so kernel shell children don’t pile up unwait4’d zombies). Orphan zombies auto-reaped by parent’s task_exit. forktest.elf and execvetest.elf migrated off busy-yielding to use sys_wait4.

Fixed

  • SYS_WRITE on fd 2 double-printed to COM1. t_putchar already mirrors to serial when g_serial_verbose is on (the default since PR #130); the explicit Serial_WriteChar loop in the FD_KIND_VGA_SERIAL case was duplicate work. Result: every chunk written by hello.elf, forktest.elf, etc. showed up twice in the serial log (“Hello, Hello, testertester!\n!\n” instead of “Hello, tester!\n”). Now only echoes explicitly when verbose is off.

Changed

  • Slice queue renumbered (CLAUDE.md + docs/roadmap.md). The old numbering had duplicate slice 13s (UTF-8 terminal and execve+wait4) and slice 14s (Per-task FD table and Userland shell), plus a “5b” hybrid that nothing else used. Queue now runs 1..21 cleanly, sub-letters reserved for tightly-grouped commit groups (15a-e for fork+COW, 16a-b for execve+wait4, 20a-f for the staged userland shell).
  • demo-script driver timeout bumped 12 s → 25 s. The script legitimately takes ~15 s of sleeps + work; the old budget was tripping a cascade into the bughunt scenarios whose windows would then absorb demo’s leftover output and start on the wrong VT.

Added — signals + preemption hardening (PR #154, slices 8 + 9)

  • Per-task signal subsystem (src/kernel/include/kernel/signal.h, src/kernel/arch/i386/proc/signal.c). Linux i386 signal numbers, sig_handler_t typedef, SIG_DFL/SIG_IGN sentinels. Per-task handler table held in a static array indexed by task-pool slot. Default-action classifier follows Linux defaults; SIGKILL bypasses mask + handler; SIGKILL/SIGSTOP can’t have their disposition overridden. Delivery hooks at schedule() (for default-terminate and SIG_IGN) and at the return-to-ring-3 path in isr_handler / irq_handler / syscall_dispatch (for user-installed handlers via a sigframe trampoline on the user stack).
  • Syscalls:
    • SYS_KILL(37)kill(pid, signo). Linux i386 number.
    • SYS_SIGNAL(48)signal(signo, handler). Returns the previous handler.
    • SYS_SIGRETURN(119) — sigframe restore; invoked indirectly by the trampoline embedded at the bottom of every sigframe.
    • SYS_FCNTL(55)F_GETFL / F_SETFL with O_NONBLOCK on stdin so non-blocking reads return -EAGAIN instead of blocking via shell_readline.
  • task_t.unkillable flag: idle (pid 1) and shell tasks set this, and sig_deliver drops SIGKILL / default-terminate signals on unkillable targets instead of marking them DEAD. Without it, maktop’s F9-picker / a stray kill from the shell could leave a VT permanently dead.
  • Per-task kticks PIT-tick accounting, incremented by timer_callback and rendered in /proc/tasks (slice 9 phase 2).
  • Runtime-tunable scheduling quantumg_sched_quantum (default 4 PIT ticks = 40 ms slice) writable via the new sched_quantum shell builtin in [1..100] (slice 9 phase 3).
  • maktop.elf — htop-style task viewer. Resolution-agnostic via sys_term_cols/sys_term_rows. Linux-style /proc/meminfo + /proc/tasks parsing, horizontal CPU% + memory meter bars (green → yellow → red), tmux-style preserved status bar at the bottom row. Auto-refreshes every 1 s; ↑↓ navigate, F9 opens a left-side signal picker (↑↓ pick a name, Enter sends), s is a numeric-entry alternate, r forces refresh, F10 / q quits. Stays POSIX-aligned: stdin set to O_NONBLOCK via fcntl, read returns -EAGAIN when the keyboard ring is empty.
  • sigtest.elf — ring-3 verifier for sys_signal + sigframe + sigreturn. Installs SIGUSR1 handler, self-sends, asserts the handler ran.
  • /proc/meminfo Linux-style fieldsMemTotal, MemFree, MemAvailable, MemUsed, plus existing heap stats. pmm_init caches the bootloader-managed frame count as the source of MemTotal.
  • task_t.fb_touched — set by SYS_PUTCH_AT / SYS_TTY_CLEAR on the calling task. shell_exec_elf checks it after the child dies and only calls shell_clear_screen() for fullscreen apps, so line-mode children (cat, hello, makbox-fallback on typos) don’t have their output wiped from the screen.
  • in_schedule re-entrancy guard in schedule(), paired with irq_save_disable / irq_restore around the critical section. Cleared before task_switch so fresh tasks (whose first execution bypasses the schedule epilogue) don’t leave the flag set forever (slice 9 phase 1).
  • ktest:
    • test_signal (31 asserts) — default-action classifier, sig_send/sig_deliver, SIGKILL override of SIG_IGN, sig_send_pid, sig_get_handler round-trip, scheduler-driven default-terminate on a spawned victim.
    • test_preempt — busy-loop victim + verify its kticks advanced (proves timer-driven preemption).
  • In-guest tests (host-driven harness, since removed):
    • ctrlc-kills-child — exec calc, send Ctrl+C, verify shell responsive (proves SIGINT default-terminate end-to-end).
    • user-sigusr1-handler — runs sigtest.elf; greps for the handler’s serial line, end-to-end ring-3 trampoline coverage.
    • no-dead-in-proctasks — proves the new procfs DEAD filter.
    • typo-doesnt-clear — proves line-mode output survives the shell’s post-exec cleanup (fb_touched gate).
  • Test-runner sync primitives (host-driven harness, since removed) — `wait_for_serial [timeout]` and an `it_until` helper. The shell emits `[shell:ready vt=N]` to serial on every `shell_readline` entry (gated by `g_serial_verbose`), so tests can sync on "the prompt is ready" instead of `sleep N` — pexpect / Tcl-Expect style. Eliminated the typo-doesnt-clear flake.

Changed (signals + preemption)

  • Ctrl+C no longer sets a kb_sigint global; the keyboard ISR now calls sig_send(focused_task, SIGINT) directly. Shell tasks install SIG_IGN for SIGINT at startup so they survive Ctrl+C at their own prompt; the \x03 byte still arrives via the keyboard ring and shell_readline handles it cooperatively (echo ^C, drop the line). Legacy keyboard_sigint_consume() shim removed.
  • shell_exec_elf no longer polls keyboard_sigint_consume() to force-kill children. Ctrl+C delivers SIGINT to the focused child; the kernel terminates it via sig_deliver; the shell just yields until t->state == TASK_DEAD.
  • Exec’d tasks now get a real name (basename of the path, minus a trailing .elf) instead of the hard-coded "exec", via the new task_t.name_buf[16] durable-storage field. Visible in /proc/tasks, cat /proc/tasks, and maktop.
  • CI run.sh outer timeout for the GDB ISO test bumped 120 s → 300 s; on FAIL, tests/groups/ktest_bg.py prints the last ktest_bg_completed/total it observed so triage doesn’t need the artifact tarball for the basic “which suite stalled” question (separate commit, same PR).

Removed (signals + preemption)

  • kb_sigint static + keyboard_sigint_consume() function (replaced by per-task signal delivery).
  • The 4-byte SCHED_QUANTUM compile-time #define (now the g_sched_quantum runtime variable).

Added

  • makbox — Makar busybox-style multicall binary (src/userspace/makbox.c). Applets: ls, cat, cp, mv, rm, rmdir, echo, pwd. Shell dispatch falls back to `makbox.elf ` after PATH lookup misses, so bare `ls` / `cat` etc. still work without symlinks (FAT32 has none).
  • SYS_GETCWD (215) — copies task_current()->cwd into a user buffer. Lets makbox pwd work without argv injection. Userspace wrapper in src/userspace/syscall.h.
  • task_t.exec_params — per-task heap-allocated exec_params_t pointer set by shell_exec_elf and consumed by exec_task_entry.
  • Shared test-runner framework (host-driven harness, since removed). Provides start_qemu / stop_qemu / it / assert_serial_contains / assert_serial_not_contains / run_test. PAUSE <secs> directive in send_script lets a single test script sleep mid-stream so child tasks (calc) are ready before the next batch of keys lands.
  • New test_makbox_pwd scenario asserts a [makbox:pwd] provenance tag emitted via SYS_WRITE_SERIAL — proves the ring-3 path ran end-to-end (not a stale builtin).

Changed

  • Shell dispatch in shell.c gains the makbox fallback after PATH lookup; first-token tab completion advertises makbox applets.
  • ls/cat/cp shell builtins removed from fs_cmds[]; tidied shell_cmd_fs.c comments throughout.
  • The host-driven test harness (since removed) reused one QEMU session across all tests (Ctrl+C / cd / reset between scenarios) — ~10× faster than the per-scenario boot model. Headless: 7/7 in ~33 s. GUI mode preserved. 30 ms per-key pacing under TCG so bursts don’t out-run the kernel’s PS/2 ring + readline pipeline.
  • GRUB interactive timeout dropped 5 s → 3 s (iso.sh, generate-hdd.sh).

Fixed

  • Cross-TTY exec race that could land a child task at a garbage EIP. Previously shell_exec_elf stashed path/argc/argv in file-static globals and handed them off to the child via the static address. Two shells exec’ing concurrently from different TTYs would clobber each other; one observed crash: panic(cpu 0): PAGE FAULT … CS=0x3F8 EIP=0xE30 PROT|READ|USER. Per-task exec_params closes the race — exec_task_entry reads from its own task’s slot, never a shared static. Reaped on slot reuse.
  • Stale g_sigint after Ctrl+Cshell_readline now drains the flag when handling KEY_CTRL_C. Previously a buffered SIGINT could leak into the next shell_exec_elf, killing the child task immediately on its first iteration so makbox/exec invocations produced no output and the shell just reprinted ^C.

Removed

  • Standalone ls.elf / echo.elf / rm.elf / mv.elf / cp.elf (now applets in makbox.elf).
  • src/kernel/arch/i386/shell/shell_cmd_fileops.c (rm/rmdir/mv now in makbox).

0.5.0 — 2026-05-14

#129. First tagged release.

Added

  • Per-TTY vt_buf_t backing grids. Writes go to the grid first; the framebuffer is only painted when that TTY is focused. Background TTYs accumulate output without contention.
  • tmux-style status bar at the reserved bottom row.
  • Synthetic /proc filesystem with cpuinfo, meminfo, tasks, uname as read-only files generated on demand.
  • Glob (*, ?) and cross-FS tab completion via vfs_complete.
  • MAKAR_VERSION single-source build constant.

Changed

  • Alt+F1–F4 switch repaints via deferred FB drain (vtty_drain_pending), out of IRQ context.

2026-05-13 — Test infrastructure

Changed

  • ccache toolchain image (makar-build:local). Warm rebuilds ~3× faster (16.9 s cold → 5.6 s warm).
  • Single-kernel/two-ISO emit (makar.iso interactive, makar-test.iso CI test_mode). KERNEL_ARGS injection lands here too.
  • Build-once fan-out CI: 4 parallel jobs (ktest, gdb-iso, gdb-hdd, in-guest tests).
  • KVM gated behind MAKAR_USE_KVM=1, off by default (CI reproducibility).

#125.

2026-05-12 — Keyboard hardening

Added

  • Typematic-repeat filter for modifier keys.
  • PS/2 LED sync (0xED <bitmap> → controller); boot-time LED state read.

Fixed

  • unsigned char audit complete across the dispatch path — no more sign-extension hazards on sentinel compares.

#127.

2026-05-12 — Keyboard rewrite

Changed

  • Full PS/2 set-1 (+ 0xE0 prefix) decoder. Layered pipeline: scancode → keycode → ASCII/sentinel → router.
  • IRQ-driven per-TTY SPSC rings (up to KB_TASK_SLOTS=4), strict make/break separation, modifier state held at the decoder.
  • Sentinels (0x800x96) for arrows, F-keys, modifier events.

Added

  • kbtester.elf — live ring-3 diagnostic dumping every event (scancode/keycode/sentinel/modifier) to serial.

#124.

2026-05-08 — Preemptive tasking

Added

  • Preemptive 100 Hz scheduler (PIT IRQ 0 yields every SCHED_QUANTUM=4 ticks).
  • Per-task task_t plumbing — pid, cwd, tty, signal bitmasks, fd_table placeholder.
  • SYS_WRITE_SERIAL (211).
  • Humanised uptime builtin.

Fixed

  • Dead-task user-PD reaper (deferred free on slot reuse) closes a UAF that occurred when a timer IRQ landed inside vmm_free_pd.

#123.

2026-05-02 — FAT32 userspace fileops

Added

  • Syscalls 208–210: SYS_DELETE_FILE, SYS_RENAME_FILE, SYS_DELETE_DIR.
  • Shell builtins rm / rmdir / mv and ring-3 apps rm.elf / mv.elf / cp.elf.

#120.

2026-05-02 — Multi-TTY

Added

  • Four independent shell tasks (shell0shell3), Alt+F1–F4 to switch.
  • Pane-aware VICS editor.
  • lsman / man <cmd> replace help.

#119.

2026-05-01 — Userspace

Added

  • Linux i386 ABI userspace. Ring-3 protected mode via iret, ELF loader (elf_exec) with argc/argv, syscall surface.
  • exec shell command.

#118.

2026-05-01 — Shell polish

Added

  • GRUB menu (default Makar OS, fallback to next bootable device).
  • 720p VESA default (-vga std).
  • Ctrl+C sigint, tab completion, calc.elf.

#117.

2026-04-30 — Build consolidation

Changed

  • All build/test/boot operations now go through run.sh (single entrypoint). Replaces the prior tangle of docker-compose targets and manual qemu invocations.

#116.

2026-04-29 — HDD path + auto-release

Added

  • Installed-HDD boot path: image generation (generate-hdd.sh), interactive boot, GDB test.
  • Auto-release workflow on main merge.

#114, #115.

2026-04-29 — VESA panes

Added

  • VESA pane abstraction (vesa_pane_t) — phase 1 of split-pane support.

#113.

2026-04-26 — Display & I/O

Added

  • Panic screen with register dump + boot-screen logo.
  • Readline inline editing.
  • File-I/O builtins.
  • Ring-3 stdin support.
  • exec waits for the child task before returning to the prompt.

#110, #111, #112.

2026-04-25 — VMM + ring-3

Added

  • Per-task page directories (vmm_create_pd, vmm_map_page, vmm_switch).
  • Ring-3 entry trampoline (ring3_enter).
  • Comprehensive ktest suites for memory + ring-3 prereqs.

Fixed

  • Cross-platform build under macOS arm64 (Docker image targeting).

#53.

2026-04-12 — Filesystem + name

Added

  • FAT32 + Medli filesystem layout (#26/#47).
  • Universal VFS layer with auto-mount.
  • / shell history.
  • Installer.
  • Build-timestamp banner.

Changed

  • Project name takes shape — shell vocabulary deliberately mirrors Medli (#48).
  • Docker image swapped for the i686-elf cross-compiler.
  • Shell split into focused source files.

2026-04-11 — Multitasking + storage

Added

  • Cooperative multitasking + int 0x80 syscall stub (#42).
  • ATA PIO IDE driver (#41).
  • MBR + GPT partition probe + mkpart (#45).

Changed

  • Source tree reshuffle: kernel moves under src/kernel/arch/i386/.

2026-04-12 — Memory + ACPI

Added

  • Paging cleanup, ACPI support, on-demand ktest harness (#44).

2026-04-09 — Rebirth

Changed

  • Project rename to “Untitled OS”. GitHub Actions wired up (push triggers, Copilot-assisted PR cadence #1–#17). The build acquires real CI, the kernel acquires real structure.

2025-01 — Single-commit hiatus

A drive-by “fix assembly comments.” commit while the rest of the codebase slept.

The project then sat largely dormant from 2020 through early 2026.

2020-08 — A brief return

Changed

  • Editorconfig, code-style consolidation.
  • Switch from GAS-style inline assembly to standalone NASM files.

Nothing shipped beyond scaffolding. Repo went quiet again after about a week.

2019-08 — First strokes

Added

  • First commit; display driver newline fix; libc stubs.
  • IDT, GDT, basic IRQ handling within the week.
  • Serial debug interface.

The project was unnamed and the build was driven by a hand-rolled QEMU shell script. Repo went quiet after about a week.

Roadmap

What’s tracked but not shipped — see roadmap for canonical status and the issue tracker for everything else.

  • Slice 8 — Linux-style signal subsystem (sigaction, kill()).
  • Slice 9 — Preemption hardening (interrupt-safe schedule()).
  • Slice 16 — VGA-text fallback per-TTY backing buffers.

Long term: musl libc port → dash → in-kernel TCC for write-compile-run on bare metal. See Userland libc.