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.
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.
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./usr/include/kernel-build/ (re-exposed kernel
headers), /usr/lib/tcc/include/{stdint,limits}.h (Makar TCC stubs).REBUILD-KERNEL via
TEST_CMDLINE='test_mode test=rebuild-kernel'.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.Host-built kernel! (GCC), Self-hosted kernel! (TCC, host build),
or Self-hosted and built inside Makar! (TCC).t->script_vars = NULL that was
inside the if (t->exec_params) block (latent leak / UAF cousin
of the kmalloc-alignment bug)./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.(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.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.
/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.task_exit rather
than panicking the kernel; panic screen names the running task on
ring-0 faults.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.vendor/tinycc/tccelf.c:fill_local_got_entries
triggered by every Makar app today (no GOT relocations).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.
POSIX process model on Makar. Detailed implementation walkthrough in internals §11; overview here.
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.#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.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.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.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 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.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).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.kticks PIT-tick accounting, incremented by
timer_callback and rendered in /proc/tasks (slice 9 phase 2).g_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 fields — MemTotal, 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).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).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).tests/ui_runner.sh sync primitives — `wait_for_serial
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..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.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).kb_sigint static + keyboard_sigint_consume() function (replaced
by per-task signal delivery).SCHED_QUANTUM compile-time #define (now the
g_sched_quantum runtime variable).src/userspace/makbox.c). Applets: ls, cat, cp, mv, rm,
rmdir, echo, pwd. Shell dispatch falls back to `makbox.elf
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.tests/ui_runner.sh — shared test-runner framework split out of
ui_test.sh. 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.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).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.tests/ui_test.sh now reuses 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.iso.sh,
generate-hdd.sh).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.g_sigint after Ctrl+C — shell_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.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).#129. First tagged release.
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./proc filesystem with cpuinfo, meminfo, tasks,
uname as read-only files generated on demand.*, ?) and cross-FS tab completion via vfs_complete.MAKAR_VERSION single-source build constant.vtty_drain_pending), out of IRQ context.makar-build:local). Warm rebuilds ~3×
faster (16.9 s cold → 5.6 s warm).makar.iso interactive, makar-test.iso
CI test_mode). KERNEL_ARGS injection lands here too.MAKAR_USE_KVM=1, off by default (CI reproducibility).#125.
0xED <bitmap> → controller); boot-time LED state read.unsigned char audit complete across the dispatch path — no more
sign-extension hazards on sentinel compares.#127.
KB_TASK_SLOTS=4), strict
make/break separation, modifier state held at the decoder.0x80–0x96) for arrows, F-keys, modifier events.kbtester.elf — live ring-3 diagnostic dumping every event
(scancode/keycode/sentinel/modifier) to serial.#124.
SCHED_QUANTUM=4 ticks).task_t plumbing — pid, cwd, tty, signal bitmasks,
fd_table placeholder.SYS_WRITE_SERIAL (211).uptime builtin.vmm_free_pd.#123.
SYS_DELETE_FILE, SYS_RENAME_FILE,
SYS_DELETE_DIR.rm / rmdir / mv and ring-3 apps rm.elf /
mv.elf / cp.elf.#120.
shell0–shell3), Alt+F1–F4 to
switch.lsman / man <cmd> replace help.#119.
iret, ELF
loader (elf_exec) with argc/argv, syscall surface.exec shell command.#118.
-vga std).calc.elf.#117.
run.sh (single
entrypoint). Replaces the prior tangle of docker-compose targets
and manual qemu invocations.#116.
generate-hdd.sh), interactive boot, GDB test.main merge.vesa_pane_t) — phase 1 of split-pane support.#113.
exec waits for the child task before returning to the prompt.vmm_create_pd, vmm_map_page,
vmm_switch).ring3_enter).#53.
#26/#47).↑/↓ shell history.#48).int 0x80 syscall stub
(#42).mkpart
(#45).src/kernel/arch/i386/.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.
Nothing shipped beyond scaffolding. Repo went quiet again after about a week.
The project was unnamed and the build was driven by a hand-rolled QEMU shell script. Repo went quiet after about a week.
What’s tracked but not shipped — see roadmap for canonical status and the issue tracker for everything else.
kill()).schedule()).Long term: musl libc port → dash → in-kernel TCC for write-compile-run on bare metal. See Userland libc.