ajhahn.de
← FlashOS
Markdown 383 lines
<div align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="assets/flashos_logo_dark.png">
    <img src="assets/flashos_logo_light.png" alt="FlashOS" width="280">
  </picture>

<h1>Changelog</h1>

<p><i>All notable changes to FlashOS, release by release.</i></p>

<p>
    <a href="README.md"><b>README</b></a> ·
    <a href="DOCUMENTATION.md"><b>Documentation</b></a> ·
    <a href="SETUP.md"><b>Setup</b></a> ·
    <a href="PORT.md"><b>Port</b></a> ·
    <a href="VERSIONING.md"><b>Versioning</b></a> ·
    <b>Changelog</b> ·
    <a href="LICENSE.md"><b>License</b></a>
  </p>
</div>

---

All notable changes to FlashOS are recorded in this file. The format is
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
(see [VERSIONING.md](VERSIONING.md)). Per-tag notes also appear on the
[releases page](https://github.com/ajhahnde/FlashOS/releases).

## [Unreleased]

## [v0.7.0] - 2026-06-18

### Added

- **`/bin/edit` — a full-screen text editor.** `edit <file>` slurps a file
  into a heap-backed gap buffer, takes over the console with the alternate
  screen + raw mode, and edits it in place: arrows / Home / End / PgUp / PgDn
  navigate, printable keys insert, Backspace / Delete remove, Enter splits the
  line, `ctrl-O` writes, `ctrl-W` searches forward from the cursor, and
  `ctrl-X` exits (prompting to save a modified buffer). It is the **first real
  consumer of the userland heap** — the `brk` / `sbrk` syscalls behind flibc's
  bump `malloc`, which already existed — so it adds **no new syscall**
  (`NR_SYSCALLS` holds) and **no kernel change**; it is pure userland. Save is
  **unlink + create + write** rather than in-place, because the FAT32 backend's
  `write` only grows `file_size` (there is no truncate): recreating the file
  yields the correct, possibly smaller, size every time. The editing logic is
  three new pure, host-tested flibc cores — `gapbuf.GapBuf` (storage),
  `gapbuf.LineIndex` (lines + cursor motions), `gapbuf.Viewport` (scroll) —
  plus a reused `grep_match.find` for search; the `keys` VT100 decoder gained
  Delete / Home / End / PgUp / PgDn and the `ctrl-O/W/X` chords. Like
  `/bin/less` it is interactive, so its edit loop is validated on real Pi
  hardware and kept out of the CI boot script. Limits (deferred): one
  logical line per screen row (horizontal scroll, no soft-wrap), no undo, tabs
  shown as a single space, fixed 24×80 geometry.

## [v0.6.0] - 2026-06-18

### Added

- **FAT32 file create / unlink / rename and the `cp` / `mv` / `rm`
  coreutils.** Three new syscalls complete the FAT32 write path:
  `create` (slot 53) makes a new empty file and returns a writable fd —
  the create-then-write half the flag-less `open` ABI lacked — `unlink`
  (54) tombstones a file's directory entry and frees its cluster chain,
  and `rename` (55) rewrites an 8.3 name in place within the same
  directory. They are built on five new pure `fat32` primitives
  (`findFreeDirSlot`, which extends a full directory by a cluster;
  `writeDirEntry`; `markDeleted`; `freeChain`; `fsInfoOnFree`) and three
  appended VFS vtable slots with EROFS defaults, so the read-only
  initramfs and any future mount stay non-destructive. Three new
  `/bin` coreutils consume them: `cp` (open + create + copy), `rm`
  (unlink each argument), and `mv` (same-directory `rename`, with a
  copy+unlink fallback across directories). Files only — `mkdir` /
  `rmdir`, sub-directory writes, and cross-directory rename are deferred.
  Created files are caller-owned (the permission metadata is a
  per-session value that does not persist across reboot). On-device
  source files use the 8.3-safe `.fl` extension (`.flash` does not fit an
  8.3 short name); there is no LFN.
- **`/bin/grep`.** A literal-pattern line search — `grep [-i] PATTERN
  [FILE...]`, reading stdin when given no file — over a pure, host-tested
  substring matcher with optional ASCII case folding. No regex, no
  new syscall.
- **`[TEST] fs-roundtrip` now exercises the full create/unlink/rename
  lifecycle.** On a mounted (real-Pi) boot the scenario folds in a CRUD
  leg — create, write, read back, rename, unlink, verify-gone — that
  leaves the disk unchanged, so it adds no new scenario and the boot
  contract stays at 30 EL0 scenarios / 34 checkpoints. FAT32 remains
  Pi-only (QEMU's EMMC2 does not pass CMD8), so the logic is covered by
  host tests and the lifecycle is validated on real Pi-4 hardware.

## [v0.5.0] - 2026-06-17

### Added

- **Hardware-monitoring syscalls and `/bin/cpuinfo`.** Four new
  argument-free syscalls expose live system metrics to user space:
  `mem_total` (slot 49, the allocatable pool size in pages), `uptime`
  (50, seconds since boot from the architectural counter), `cpu_temp`
  (51, SoC temperature in milli-degrees Celsius) and `cpu_freq` (52, the
  ARM core clock in Hz). Temperature and clock are read over the VideoCore
  mailbox (a new `TAG_GET_TEMPERATURE` property tag) and return `0` =
  unknown on a board without the firmware (QEMU virt) or on a mailbox
  timeout, which the tools render as `n/a` — never a fabricated value. Two
  new coreutils join the existing `/bin/meminfo`: `/bin/cpuinfo` prints the
  temperature and clock, and `/bin/uptime` prints the time since boot as
  `Nm Ns`. `/bin/sysinfo` gains `mem` (used/total, the used figure in KiB
  so a small footprint no longer floors to `0 MiB`), `uptime`, `temp` and
  `freq` rows. Two in-kernel `[TEST]` scenarios (`hwmon-core`,
  `hwmon-mailbox`) cover the new syscalls, moving the boot contract to 30
  EL0 scenarios and 34 per-scenario checkpoints.

### Fixed

- **VideoCore mailbox reads no longer match a stale reply.** The doorbell
  word the kernel posts to the property channel is identical on every call
  (a fixed property-buffer address OR-ed with a fixed channel), so the
  completion check could not tell a fresh reply from the leftover of the
  previous transaction. Two back-to-back reads — as `cpuinfo` and
  `sysinfo` issue for temperature then clock — let the second read consume
  the first read's stale FIFO entry and parse the property buffer before
  VideoCore had refreshed it, surfacing as the clock flapping between its
  real value and `n/a`. `transact()` now drains the read FIFO before
  posting and brackets the doorbell exchange with a `dsb sy` barrier, so
  each read observes only its own reply; verified on real hardware with
  three consecutive `cpuinfo` runs holding a stable `600 MHz`.
- **`/bin/login` now refuses to run as a non-root command.** Invoked from
  an already-privilege-dropped shell, `login` would still authenticate the
  entered credentials — the kernel verifier does not gate on the caller's
  uid — and only then fail the privilege drop with a misleading `login:
  cannot drop privilege`, a confusing half-success that could never grant a
  higher uid since `setuid` is one-way. `login` now checks `geteuid()` at
  entry and exits with `login: must be root`, leaving session minting to the
  PID-1 supervisor. Switch users by logging out back to that supervisor,
  then logging in as the other account.

## [v0.4.0] - 2026-06-13

### Changed

- **The OS-image source is now written in
  [Flash](https://github.com/ajhahnde/Flash).** The kernel core, board
  drivers, user space, and the in-kernel test harness were ported from
  Zig to Flash — a self-hosted systems language that transpiles to Zig —
  and now carry the `.flash` extension. `flashc` lowers them to Zig at
  build time, so the kernel image is behaviourally identical to the
  pre-port build: both boot watchdogs assert the same 28-scenario /
  32-checkpoint contract and the same per-board free-page checkpoints,
  with no re-capture across the port. See [PORT.md](PORT.md) for the full
  lineage.
- **New build dependency: the Flash compiler (`flashc`).** Builds now
  transpile the `.flash` modules through `flashc`, pinned by
  `flash-toolchain.lock` and resolved via `-Dflashc=<path>` (default
  `~/Flash/zig-out/bin/flashc-stage1`). Build it once from the pinned
  commit — see [Setup §1](SETUP.md#1-host-toolchain). The AArch64
  assembly, the tracing subsystem, the host build tooling, and two kernel
  modules awaiting a compiler feature stay Zig (see
  [PORT.md §4](PORT.md#4-what-stays-zig)).

## [v0.3.0] - 2026-06-07

### Added

- **Password masking at the login prompt.** `/bin/login` now echoes a
  `*` per typed password character instead of suppressing echo, via a new
  `CONSOLE_MODE_MASK` bit on `SYS_SET_CONSOLE_MODE`; typed secrets are
  acknowledged without being shown. The mode is restored to the default
  (kernel echo off) before the shell starts, so the mask never leaks into
  the session.
- **Shared `console_ui` terminal-look module (`lib/console_ui/`).** One
  freestanding source owns the status-tag taxonomy (`[ OK ]` / `[WARN]` /
  `[FAIL]` …), the ANSI palette, and the line/stage/banner renderers,
  compiled into both the kernel boot log and the userspace shell through a
  caller-supplied sink — the whole system restyles from a single file.
  Status tags tint only the inner word, systemd-style.
- **fsh homescreen banner.** The shell prints
  `FlashOS [v<version>] by ajhahnde - type 'help' for commands` at REPL
  entry, with the version single-sourced from `build.zig.zon` via
  `build_options` (no version literal in code).
- **TAB completion in the shell.** `fsh` completes on TAB via a new
  `readlineCompleting` line-editor path: the first token against `/bin`
  plus the in-process built-ins, a later token as a filesystem path; the
  buffer extends to the longest common prefix and a unique match appends a
  trailing space or `/`. `help` now also enumerates `/bin`, so a new tool
  advertises itself by existing.
- **`/bin/sysinfo`.** A print-and-exit coreutil rendering the FlashOS
  version, the logged-in user, and the free-page count as aligned
  key/value rows — the first consumer of the new screen-renderer module.
- **Shared screen-renderer + input seams.** New freestanding, host-tested
  modules: `lib/console_ui/screen.zig` (alternate-screen, cursor, box
  panels, key/value rows), `user_space/lib/flibc/keys.zig` (VT100 input
  decoder), and `user_space/lib/flibc/completion.zig` (tab-completion
  core). Allocator-free, with no kernel changes.
- **`reboot` and `logout` shell built-ins.** `fsh` gains `reboot`, which
  resets the board through a new `SYS_REBOOT` syscall (slot 47) — PSCI
  `SYSTEM_RESET` over the HVC conduit on QEMU `virt`, the BCM2711 watchdog
  full-reset on Raspberry Pi 4 — and `logout`, a synonym for `exit` that
  ends the session and returns to the `login:` prompt. Both join the
  shell's TAB completion and `help` listing.
- **Command history and in-line cursor editing in the shell.** `fsh`
  now decodes the VT100 arrow-key sequences in its line editor
  (`flibc.readlineEdit`) instead of echoing them as literal characters:
  Up/Down recall earlier commands from a per-session history ring, and
  Left/Right move the cursor so a keystroke inserts or backspaces at the
  cursor rather than only at the end of the line. History lives in a
  fixed, caller-owned ring (no allocator) and needs no new syscall.
- **`-Dtest-filter` for the host-test step.** `zig build test
  -Dtest-filter=<substr>` runs only host tests whose name contains the
  substring, for faster focused iteration; the default runs the full
  suite.
- **`/bin/less`.** A full-screen text pager — the first interactive
  consumer of the screen-renderer + input seams. It takes over the
  alternate screen, draws a titled panel, and scrolls a file with the
  arrow keys, `j`/`k`, space/`b` (page), and `g`/`G` (ends), quitting on
  `q` and restoring the shell view. Built on a new allocator-free,
  host-tested pager core (`flibc.Pager`); no new syscall.
- **`/bin/clear`.** A terminal-clear coreutil — the smallest consumer of
  the shared screen renderer. It emits the `console_ui.screen.clear`
  sequence (cursor home + erase) and exits, wiping the current screen in
  place; the escape bytes stay single-sourced in `console_ui` rather than
  hardcoded in the tool.
- **Double-TAB candidate listing in the shell.** When TAB completion is
  ambiguous with nothing left to insert, a second consecutive TAB lists
  every matching command or path on a fresh line and redraws the prompt,
  so the choices are visible without abandoning the typed line. Built on a
  new pure, host-tested `completion.classify` helper; no new syscall.
- **`pwd` shell built-in.** `fsh` gains `pwd`, which prints the current
  working directory through a new `SYS_GETCWD` syscall (slot 48) — the
  readback half of the existing `cd` / `SYS_CHDIR` store, copying the
  per-task `cwd` out of the kernel. It joins the shell's TAB completion
  and `help` listing.

### Changed

- **`help` output restructured for readability.** The shell's `help` now
  lists each built-in with a one-line description in aligned columns under
  section headers (`Commands:` / `Run a program:` / `Programs in /bin:`),
  replacing the previous single-line blob. The `/bin` listing is still
  enumerated live, so new tools keep appearing automatically.
- **Boot-success marker moved to the fsh homescreen.** The QEMU watchdog
  and the `picapture` helper now key on the stable
  `type 'help' for commands` homescreen tail instead of the retired
  `[ OK ] Reached target Shell` / `[ OK ] Authenticated` markers. The
  kernel entropy announce is reworded from
  `hwrng: fallback (timer mix, weak) ok` to `Initialized hwrng`. These
  change the serial console output format (a breaking change to the boot
  contract).
- **virt boot-watchdog free-page checkpoints.** They move to `0x3be46`
  (per scenario) / `0x3be54` (boot baseline) because the larger `fsh`
  (TAB completion, history, the restructured `help`), the new `/bin/sysinfo`
  and `/bin/less` tools, and the `+strict-align` codegen grow the embedded
  initramfs and kernel image; rpi4b is unchanged (its reserve calls are
  no-ops).

### Removed

- **`[ OK ] Authenticated` login marker.** `/bin/login` no longer prints
  a per-session auth marker; a blank line separates the password prompt
  from the shell homescreen. Boot success is now the homescreen-marker
  count alone.

### Fixed

- **`/bin/less` alignment fault on real hardware.** The pager's by-value
  `Pager` return was vectorized into a misaligned 16-byte NEON store
  (`stur q` at struct offset 40), which faulted under `SCTLR_EL1.A` on real
  silicon (data abort, alignment fault) while passing QEMU's lenient TCG.
  Instead of another per-site `align(16)` / volatile dodge, the freestanding
  aarch64 build target now sets `+strict-align`, so LLVM never widens a copy
  or a by-value return into an unaligned NEON store — closing the class for
  the kernel and every userland tool at codegen. The virt boot-watchdog
  checkpoints shift one page as a result (see Changed).
- **Line editing at the `login:` and password prompt.** `/bin/login` read
  the username and password through a dumb byte loop that *appended* a
  backspace byte instead of erasing it, so a single mistype was
  uncorrectable and the attempt failed as "Login incorrect." Login now
  drives flibc's line editor in echo-off mode: the username gets full
  backspace editing, and the password reuses the same host-tested `step`
  core with masked echo — one `*` per byte, rubbed out on backspace. No
  kernel or syscall change.

## [v0.2.0] - 2026-06-06

### Added

- **FAT32 subdirectory path traversal.** Files below the mount root
  (`/mnt/dir/file`, and deeper) now open. Previously the mount backend
  encoded the whole mount-relative path as a single 8.3 name, so any
  `/`-separated path was rejected and only files in the mount root were
  reachable. The open hook now walks the path one component at a time,
  descending into each subdirectory entry. Directory *listing*
  (`readdir`) stays root-only for now — opening a known path works.
- **FAT32 write to an empty file.** Writing to a file whose directory
  entry has no data cluster yet (`first_cluster == 0`, the on-disk shape
  of a 0-byte file) now allocates its first cluster, links it, and
  records it in the directory entry, instead of failing closed. The
  write path now identifies the file by its directory-entry location
  (stashed at open) rather than an ambiguous re-walk by first cluster, so
  file growth works for subdirectory files too. Covered by host tests and
  a Pi-only `[TEST] fs-empty-write` in-kernel scenario; the boot contract
  moves to **28 in-kernel scenarios / 32 per-scenario checkpoints** (was
  27 / 31). Create-if-missing for a *non-existent* path and crash-atomic
  writes remain future work.
- **`-Dboot-selftest` build option (default off).** Gates the in-kernel
  test harness: a normal `zig build run-virt` / `deploy` now boots
  straight to the `login:` prompt with no test output, while CI and
  validation builds pass `-Dboot-selftest=true` to run the full
  in-kernel scenario suite. The EMMC2 smoke test and the free-page
  checkpoint dump are gated the same way.

### Changed

- **Boot output restyled to systemd-style status lines.** The kernel,
  init, login, and fsh now print `[ OK ]` / `[SKIP]` / `[WARN]` lines
  instead of `[Debug]` noise. The two success markers were renamed —
  `[Debug] login OK``[ OK ] Authenticated` and
  `[Debug] fsh init OK``[ OK ] Reached target Shell` — so any log
  parser keying on the old strings must be updated. The boot-contract
  checkpoint and session counts are unchanged by the restyle.
- **Diagnostic output suppressed by default.** EMMC2 and USB bring-up
  traces and hwrng chatter are now gated behind in-file flags
  (`DIAG`, `TRACE_VERBOSE`) that default off, keeping the boot log clean.

## [v0.1.0] - 2026-06-05

First public release. FlashOS was developed privately before this
release, so v0.1.0 already ships a substantial feature set; the
highlights are below.

### Added

- **Bare-metal AArch64 kernel** for the Raspberry Pi 4B and QEMU
  `-M virt`. Two-stage boot to EL1 (an EL3 armstub sets up the GIC and
  `eret`s down on the Pi; `src/boot.S` drops straight from EL3 to EL1
  under QEMU), then a four-level 4 KiB-page MMU brings up the identity
  map, the linear-high kernel map, and demand-allocated user pages.
- **Scheduler and processes.** Priority round-robin with timer-driven
  preemption, and the full `fork` / `exec` / `exit` / `wait` / `kill`
  lifecycle over an indexed syscall table. Zombies are reaped, and the
  scheduler stays leak-free across stress cycles.
- **ELF loader and a small userland libc** to run programs from user
  space, with bounds checks on the segment ranges so a malformed binary
  cannot map over the kernel or the stack guard.
- **Filesystem.** An initramfs plus a FAT32 backend, backed by the SD
  card on the Pi and a disk image under QEMU. A write/verify roundtrip
  test runs during boot.
- **Interactive shell (`fsh`)** over a unified file-descriptor ABI, with
  pipes, console RX, and tracing. Memory pressure is handled gracefully.
- **USB-C gadget console.** The Pi enumerates as a USB CDC serial device,
  so `fsh` runs over the same C-to-C cable that powers the board; no
  separate serial adapter is required for normal use.
- **Logins.** A small identity/auth layer: a `login:` prompt,
  PBKDF2-hashed passwords in a shadow file, and `passwd` to change them.
  The accounts are build-time and public; the security model and its
  limitations are documented.
- **Opt-in profiler** behind `-Dtrace`: samples the interrupted PC each
  timer tick and prints a symbolized trace on the Mini-UART. Off by
  default, zero footprint when off (the default image is byte-identical).
- **Tests.** An in-kernel `[TEST]` harness (27 EL0 scenarios / 31
  checkpoints, each with a free-page baseline check) that runs on every
  boot on both targets, plus a host-side `zig build test` suite (361
  tests across 35 modules) for the pure-logic pieces. CI runs the boot
  path on every push.
- **Dual-target build**`-Dboard=rpi4b` / `-Dboard=virt` swaps the
  per-board driver set, linker script, and boot quirks at comptime. Both
  targets boot cleanly.
- **Kernel symbol table** generated from the linked ELF by a two-pass
  build step, so panics and the profiler can print real names.

[Unreleased]: https://github.com/ajhahnde/FlashOS/compare/v0.7.0...HEAD
[v0.7.0]: https://github.com/ajhahnde/FlashOS/compare/v0.6.0...v0.7.0
[v0.6.0]: https://github.com/ajhahnde/FlashOS/compare/v0.5.0...v0.6.0
[v0.5.0]: https://github.com/ajhahnde/FlashOS/compare/v0.4.0...v0.5.0
[v0.4.0]: https://github.com/ajhahnde/FlashOS/compare/v0.3.0...v0.4.0
[v0.3.0]: https://github.com/ajhahnde/FlashOS/compare/v0.2.0...v0.3.0
[v0.2.0]: https://github.com/ajhahnde/FlashOS/compare/v0.1.0...v0.2.0
[v0.1.0]: https://github.com/ajhahnde/FlashOS/releases/tag/v0.1.0

---

[← Prev: Versioning](VERSIONING.md) · [Next: License →](LICENSE.md)
</content>