Tulio A M Mendes [Fri, 17 Apr 2026 21:46:32 +0000 (18:46 -0300)]
Fix job control TIOCSPGRP: allow session leader to claim TTY/PTY
When fulltest runs from the shell, the TTY's tty_session_id is already
set to the shell's session. The test's leader child calls setsid() then
TIOCSPGRP, which fails with EPERM because the new session doesn't match
tty_session_id. The kernel never allowed a new session leader to claim
the TTY as its controlling terminal.
Fix: in TIOCSPGRP handler for both tty.c and pty.c, allow a session
leader (session_id == pid) to claim the terminal by updating the
session when it explicitly sets its own pgrp as foreground. This
matches POSIX semantics where a session leader may establish a new
controlling terminal.
Test results: 103/103 smoke test PASS, 16/16 battery PASS.
Tulio A M Mendes [Fri, 17 Apr 2026 21:08:02 +0000 (18:08 -0300)]
diskfs: fix LBA space leak on unlink/rmdir + add spinlock protection + ATA DMA irqsave
Root cause: diskfs_unlink and diskfs_rmdir zeroed freed inodes but
never reclaimed LBA space -- next_free_lba only grew, never shrank.
After many test runs, next_free_lba exceeded the 8192-sector disk,
causing ata_pio_read28 to fail with -EIO on out-of-range LBAs,
making writes to newly created files fail silently.
Fixes:
- Add diskfs_reclaim_space() that recalculates next_free_lba by
scanning all inodes for the highest used LBA extent. Called from
both diskfs_unlink and diskfs_rmdir after freeing an inode.
- Add g_diskfs_lock spinlock with irqsave/irqrestore variants
protecting all superblock load/modify/store operations across all
diskfs functions. All early returns properly unlock.
- Restructure diskfs_read_impl and diskfs_write_impl to cache inode
metadata under lock, then release before sector I/O, avoiding long
interrupt-disabled periods that could cause deadlocks/timeouts.
- Replace spin_lock/spin_unlock with spin_lock_irqsave/spin_unlock_
irqrestore in all 4 ATA DMA channel functions to prevent deadlocks
from timer interrupts during lock hold.
Tulio A M Mendes [Fri, 17 Apr 2026 17:01:34 +0000 (14:01 -0300)]
Fix fulltest TIOCGPGRP failure when run from shell
When fulltest runs from /bin/sh (rather than as init directly), the
shell has already claimed the controlling TTY via setsid(), setting
tty_fg_pgrp to a non-zero value. The test assumed fg == 0, which
only holds when running as PID 1 (session_id=0, pgrp_id=0).
Fix: compare TIOCGPGRP result against sys_getpgrp() instead of
hardcoded 0, use our own pgrp for TIOCSPGRP success test, and use
fg=-1 (EINVAL) instead of fg=1 for the expected-failure case.
Also rename 'hello from init.elf' to 'hello from fulltest.elf' in
both the test binary and the smoke test expectations.
Tulio A M Mendes [Fri, 17 Apr 2026 04:09:18 +0000 (01:09 -0300)]
fix(mm): munmap/brk page leaks — free physical frames on unmap, rollback on partial failure
1. munmap: vmm_unmap_page only clears PTEs without freeing physical
frames. For anonymous mmaps (shmid == -1), call vmm_virt_to_phys +
pmm_free_page before vmm_unmap_page. Device-backed/shared mappings
keep their frames (managed by their own subsystems).
2. brk shrink: same leak — free physical frames before unmapping when
the heap shrinks.
3. brk grow partial failure: if pmm_alloc_page fails mid-expansion,
rollback all pages already mapped in this call (unmap + free), then
return old heap_break. Previously these pages were leaked permanently.
4. mmap anonymous partial failure: same rollback pattern applied to
syscall_mmap_impl — if pmm_alloc_page fails mid-allocation, unmap
and free all pages already mapped before returning -ENOMEM.
All tests pass: 69/69 host + 103/103 QEMU, zero regressions.
Tulio A M Mendes [Fri, 17 Apr 2026 03:54:10 +0000 (00:54 -0300)]
fix: 3 residual bugs from round-3 audit
5. dlopen page leak: if pmm_alloc_page() fails mid-segment-load,
rollback all previously mapped pages (unmap + pmm_free_page)
instead of leaking them. Added vmm_virt_to_phys() API to
recover physical frames before unmapping.
6. CLONE_THREAD without CLONE_VM: Linux requires CLONE_THREAD to
imply CLONE_VM (threads share address space). Now returns
-EINVAL if CLONE_THREAD is set without CLONE_VM, preventing
unexpected behavior where a "thread" gets its own AS copy.
7. pipe_close SMP race: readers/writers decrement and conditional
kfree(ps) were unprotected by a lock. Added spinlock_t to
pipe_state and wrapped the critical section in
spin_lock_irqsave, preventing underflow/double-free when both
pipe ends are closed concurrently on different CPUs.
Tulio A M Mendes [Fri, 17 Apr 2026 03:16:43 +0000 (00:16 -0300)]
fix: 4 security/robustness bugs from audit
1. epoll: remove fd from epoll instances on close() — prevents
use-after-close where a reused fd number would monitor the
wrong file. Also auto-remove stale entries in epoll_wait.
2. clone: validate flags against CLONE_SUPPORTED_MASK — unknown
flags (CLONE_NEWPID, CLONE_NEWUSER, etc.) now return EINVAL
instead of being silently ignored.
3. futex: cleanup waiters on process exit — futex_waiters table
moved to file scope; futex_cleanup_process() called from
SYSCALL_EXIT prevents UAF when FUTEX_WAKE dereferences a
freed process pointer.
4. ksem: fix infinite spin when waiters array full — replace
schedule() spin-yield with process_sleep(1), and increase
KSEM_MAX_WAITERS/KCOND_MAX_WAITERS from 16 to 64.
Tulio A M Mendes [Thu, 16 Apr 2026 05:42:54 +0000 (02:42 -0300)]
fix(test): skip /persist/counter test when no disk is available
Instead of failing with sys_exit(1) when /persist/counter can't
be opened, skip the test with goto. This allows the fulltest to
continue running other tests in environments without persistent
storage.
Tulio A M Mendes [Thu, 16 Apr 2026 05:36:59 +0000 (02:36 -0300)]
cleanup(idt): remove debug page fault traces for PID 1
Removed debug kprintf statements that were added during the
execve/SIGSEGV debugging session: PDE/PTE dumps, CR3 comparison,
GOT dumps, and LAPIC ID traces. These are no longer needed now
that the root cause has been fixed.
Tulio A M Mendes [Thu, 16 Apr 2026 05:36:47 +0000 (02:36 -0300)]
fix(ulibc): add null-pointer guards in __libc_init_array/fini_array
In a PIC shared library build, linker-generated symbols like
__init_array_start, __fini_array_end, and _init may remain
SHN_UNDEF (value 0) when the library has no .init_array/.fini_array
sections. The GOT entries for these resolve to 0.
Added null-pointer checks using volatile pointers (to prevent the
compiler from assuming the address is always non-null) for all
init/fini array pointers and for _init/_fini function pointers.
This prevents crashes when these symbols are unresolved.
Tulio A M Mendes [Thu, 16 Apr 2026 05:36:15 +0000 (02:36 -0300)]
fix(elf): skip SHN_UNDEF symbols in GLOB_DAT/32 relocations
The kernel's elf32_process_relocations incorrectly resolved R_386_GLOB_DAT
relocations for SHN_UNDEF symbols (st_shndx == 0) by setting the GOT
entry to base_offset. For libc.so loaded at 0x11000000, this meant
symbols like __init_array_start, __fini_array_end, and _init were
resolved to 0x11000000 (the ELF header), causing __libc_init_array
to execute garbage code at the ELF header and crash with SIGSEGV
at eip=0x11000012.
Now, SHN_UNDEF symbols are set to 0 in the GOT, leaving them for
the dynamic linker to resolve. Defined symbols (st_shndx != 0)
continue to be resolved as sym->st_value + base_offset.
Also removed debug kprintf from elf32_load_needed_libs.
Tulio A M Mendes [Thu, 16 Apr 2026 05:35:55 +0000 (02:35 -0300)]
fix(ld.so): use correct AdrOS syscall numbers instead of Linux numbers
dbg_write() was using Linux syscall number 4 (write) in EAX, but
AdrOS's SYSCALL_WRITE is 1. This caused dbg_write to call open()
instead of write(), so ld.so debug output never appeared.
Also fixed the inline asm to properly declare EAX as an output
operand (int $0x80 returns the result in EAX), preventing potential
compiler misoptimization.
Removed all debug prints from _start_c and dl_fixup that were
added during debugging.
Tulio A M Mendes [Thu, 16 Apr 2026 05:35:36 +0000 (02:35 -0300)]
fix(execve): reset signal handlers and clear pending signals on execve
POSIX requires that execve resets all signal handlers to SIG_DFL
(except SIG_IGN) and clears pending signals. Without this, the
kernel could attempt to deliver a signal to a handler address in
the destroyed old address space, causing a crash.
Also removed debug trace code (ldso trace, write trace, exit trace,
PTE dumps, GOT dumps) that was added during debugging and was
interfering with test output.
- Replace plain integer 0 with NULL for pointer assignments, returns,
and comparisons across 24 source files (kernel, drivers, HAL, arch, net)
- Add missing <stddef.h> includes for NULL in keyboard.c and uart.c
- Make file-local symbols static in idt.c (idt, idtp, interrupt_handlers,
idt_set_gate, pic_remap, print_reg) and scheduler.c (ready_queue_tail)
- Add missing extern/prototype declarations to headers:
idt.h (isr_handler), syscall.h (syscall_handler),
process.h (ready_queue_head, sched_lock, sched_set_init_pid,
sched_assign_pid1, sched_ap_init), net.h (net_dhcp_start),
percpu.h (_percpu_gs_lut, percpu_get_ptr), smp.h (ap_sched_go,
ap_entry), arch_syscall.h (x86_sysenter_set_kernel_stack,
sysenter_init_ap), hal/usermode.h (x86_usermode_test_start),
hal/cpu.h (g_smap_enabled), ata_pio.h (ata_register_devfs),
boot_info.h (kernel_main)
- Add forward declarations for compiler ABI symbols in utils.c
- Include own headers in source files so sparse sees declarations
- Add lwIP/net include paths to SPARSE_FLAGS in Makefile
Verified: make check (0 warnings), make test-host (116/116 pass),
make test (102/103 pass, 1 pre-existing timeout)
fix(security): mq_receive_impl TOCTOU race — copy msg data under lock
Mirror the mq_send_impl fix: copy message data into a kernel buffer
while holding mq_lock, then copy_to_user after release. Without this,
another thread could overwrite the dequeued slot via mq_send before
the copy_to_user completes.
Found during deep re-scan. All tests pass (102/103, 1 pre-existing timeout).
fix(security): Red Team bug fixes + deep analysis hardening
Red Team report fixes (8 bugs):
- pmm_free_page: add rc==0 double-free guard with BUG log
- uaccess: move global state to per-CPU via GS segment (SMP safety)
- elf.c: add overflow checks for p_vaddr+base_offset and vaddr+p_memsz
- ksem_wait: retry loop on waiter-array-full instead of silent discard
- kstack: add free-slot recycling stack (256 entries) to prevent leak
- schedule(): fix fallback dequeue from active (not expired) after swap
- buddy_of(): add bounds check, kfree handles NULL return
- e1000: add compiler memory barriers to MMIO read/write helpers
Deep analysis fixes (4 additional bugs):
- readdir: fix strcpy buffer overflow d_name[24] from name[128] in
tmpfs, devfs, initrd (use strncpy + null termination)
- mq_send_impl: fix TOCTOU race — copy user data to kbuf before lock
- dlopen: add 32-bit overflow checks for p_vaddr+base and vaddr+p_memsz
- tmpfs/overlayfs: replace unbounded strcpy into vfs.name[128] and
symlink_target[128] with strncpy
Regression fix:
- percpu: add self-pointer as first field of percpu_data so that
percpu_get() returns a valid pointer (was returning cpu_index=0 as
NULL pointer on BSP, causing triple fault)
- Add new section 2 'Getting the Source' with git clone --recursive
and fallback 'git submodule update --init --recursive' instructions.
- Note that the build system auto-initializes submodules and applies
patches if not done manually.
- Fix DOOM section: reference submodule init instead of manual git
clone; simplify to 'make doom'.
- Renumber sections 3-8 to accommodate the new section.
feat(toolchain): dual-libc support — ulibc default, Newlib optional
Redesign the toolchain build to safely support both ulibc (dynamic
linking) and Newlib (static linking) with a safe-by-default approach:
Architecture:
- adros.h now has Newlib-safe defaults (LIB_SPEC with -ladros, no
dynamic-linker in LINK_SPEC) so a fresh 'git clone' + 'build.sh'
always produces a working toolchain even without prior 'make iso'.
- Step 4b: generates specs from dumpspecs (Newlib-compatible).
- Step 4c: if ulibc build artifacts exist, re-patches specs for ulibc
(removes -ladros, adds -dynamic-linker /lib/ld.so).
- Step 4d: creates newlib.specs + i686-adros-gcc-newlib wrapper for
optional Newlib builds.
Bug fixes for fresh-clone reproducibility:
- Newlib build skip check changed from libc.a (overwritten by ulibc)
to a .built marker file.
- Newlib headers/libs backed up unconditionally (before ulibc check)
so newlib.specs always has valid include/newlib/ directory.
- Bash cross-compilation now uses newlib.specs explicitly (Bash needs
Newlib's full POSIX libc: regex, locale, wchar).
- Wrapper script uses expanded paths instead of hardcoded GCC version.
Usage:
i686-adros-gcc -o out in.c # ulibc dynamic (default)
i686-adros-gcc -static -o out in.c # ulibc static
i686-adros-gcc-newlib -o out in.c # Newlib static
feat(ulibc): add missing fdopen() and fileno() to stdio
Add POSIX fdopen() and fileno() implementations required by
popen/pclose and other stdio operations:
- fdopen(): wraps an existing file descriptor into a FILE*
- fileno(): returns the file descriptor from a FILE*
feat(ulibc): add __libc_init_array/__libc_fini_array and update crt0.S
Add constructor/destructor support to ulibc:
- New file init_fini.c: implements __libc_init_array() to run .preinit_array
and .init_array global constructors, and __libc_fini_array() to run
.fini_array global destructors.
- Updated crt0.S: calls __libc_init_array before main and __libc_fini_array
after main returns (matching Newlib crt0 behavior). Also uses 'environ'
symbol (POSIX) and adds .note.GNU-stack for NX compatibility.
fix(ulibc): rename __environ to environ for POSIX compliance
Replace __environ with environ as the primary environment pointer
symbol throughout ulibc and userland commands. POSIX specifies
'environ' as the standard name (IEEE Std 1003.1).
Files changed:
- user/ulibc/src/stdlib.c: environ as primary symbol, update getenv()
- user/ulibc/src/environ.c: all references updated
- user/ulibc/src/execvp.c: pass environ to execve()
- user/ulibc/include/unistd.h: declare extern environ
- user/cmds/env/env.c: use environ
- user/cmds/printenv/printenv.c: use environ
- Move AdrOS sysroot headers installation to AFTER Newlib install so
Newlib's make install doesn't overwrite our custom sys/dirent.h
(fixes DIR unknown type error on first build)
- Move Newlib header patching (sys/stat.h, sys/signal.h, sys/wait.h,
glob.h) to after Newlib install (fixes sed on non-existent files)
- Keep libcody in GCC host_libs — required by cc1plus (C++ frontend)
- Add all-libcody to step 4 make targets
- Move specs file creation to after step 4 (GCC full install overwrites it)
- Add .note.GNU-stack to crti-adros.S (fixes executable stack warning)
- Fix *.h glob in libgloss/adros cp (no .h files exist)
- Add toolchain/build/ and toolchain/logs/ to .gitignore
- Update README.md: clarify patches/ are reference diffs, add native-toolchain.patch
Tulio A M Mendes [Sat, 14 Mar 2026 16:14:55 +0000 (13:14 -0300)]
fix: doom build + move userspace build output to build/$ARCH/user/
- Fix doom.elf build: force i686-elf toolchain (CC/LD := instead of ?=),
add --unresolved-symbols=ignore-in-shared-libs for libc.so undefined refs
- Reorganize userspace build output from build/user/ to build/$ARCH/user/
(e.g. build/x86/user/) so all arch artifacts live under one directory
- Sub-Makefiles (common.mk, fulltest, ldso, pie_test) now accept BUILDDIR
override from root Makefile via ?= operator
- Root Makefile passes BUILDDIR=$(CURDIR)/$(USER_BUILD)/cmds/<name> to
all sub-Make invocations
Tulio A M Mendes [Sat, 14 Mar 2026 15:40:11 +0000 (12:40 -0300)]
refactor: reorganize userland into user/cmds/<name>/ with per-program Makefiles
- Move 53 user commands from user/<name>.c to user/cmds/<name>/<name>.c
- Add user/cmds/common.mk shared build rules for dynamically-linked commands
- Add per-program Makefiles for all commands (including fulltest, ldso, pie_test)
- Build all .o/.elf into build/user/cmds/<name>/ (out-of-tree)
- Replace [init] test prefix with [test] in fulltest.c, pie_main.c, test scripts
- Fix find.c and which.c: use opendir/readdir/closedir instead of raw getdents
- Fix ulibc glob.c missing stdio.h include
- Fix ulibc -Wno-incompatible-pointer-types for GCC 14+
- Fix test_host_utils.sh which test set -e issue
- Doom rootfs path changed to /usr/games/doom
- make clean now also cleans ulibc and doom in-tree artifacts
Tulio A M Mendes [Sat, 14 Mar 2026 13:23:54 +0000 (10:23 -0300)]
feat: full SMP scheduling — AP tick accounting, IPI wakeups, load balancing
Scheduler changes (src/kernel/scheduler.c):
- sched_ap_tick(): per-CPU tick accounting for APs (utime, ITIMER_VIRTUAL,
ITIMER_PROF) — previously only BSP tracked CPU time
- sched_load_balance(): periodic work stealing — migrates one process from
busiest to idlest CPU when imbalance >= 2 (avoids ping-pong)
- IPI resched after sleep wakeups: process_wake_check() now sends IPI to
remote CPUs that received newly-ready processes from the sleep queue
- IPI resched after parent wakeup: process_exit_notify() sends IPI when
waking a parent blocked in waitpid on a different CPU
- Load counter (sched_pcpu_inc_load) added to wakeup paths that were
missing it (exit_notify parent wake, sleep queue wake)
Timer changes:
- src/drivers/timer.c: hal_tick_bridge() now calls sched_ap_tick() on APs
and sched_load_balance() on BSP every 10 ticks (~200ms at 50Hz)
- src/hal/x86/timer.c: APs now go through hal_tick_bridge instead of
calling bare schedule(), enabling proper AP tick accounting
- Uses percpu_cpu_index() (GS segment read) instead of smp_current_cpu()
(LAPIC ID linear scan) for faster CPU identification
Tests:
- New SMP parallel fork smoke test: forks 8 children with busy loops,
verifies all complete with correct exit status (exercises multi-CPU
scheduling, IPI wakeups, and load balancing)
- 102/102 smoke tests pass, cppcheck clean, 64/64 host tests pass
Tulio A M Mendes [Sat, 14 Mar 2026 09:11:24 +0000 (06:11 -0300)]
refactor: move sysroot compat headers into project + update build.sh
All 71 Linux/POSIX compatibility headers that were previously created
directly in /opt/adros-toolchain/i686-adros/include/ are now stored
in newlib/sysroot_headers/ and installed by build.sh via cp -r.
Tulio A M Mendes [Sat, 14 Mar 2026 03:26:36 +0000 (00:26 -0300)]
fix: newlib crt0.o build error and gcc-full c++tools install failure
Two fixes to toolchain/build.sh:
1. Newlib: added have_crt0="no" to the adros entry in configure.host.
Newlib's build system expected crt0.o from libc/sys/adros/, but our
crt0 is provided by libgloss/adros. Setting have_crt0=no tells newlib
to skip the crt0.o copy. Also replaced the old Makefile.am/configure.in
(recursive autotools, wrong for newlib 4.4.0) with a minimal Makefile.inc
(non-recursive build system).
2. GCC full rebuild: use explicit targets (all-gcc all-target-libgcc,
install-gcc install-target-libgcc) instead of bare 'make install'.
The bare install fails on c++tools/g++-mapper-server which is not
needed for cross-compilation and doesn't build for freestanding
targets.
Verified:
- i686-adros-gcc 13.2.0 and i686-adros-g++ 13.2.0 both work
- C and C++ cross-compilation succeeds
- newlib libc.a and libm.a installed
- 100/100 smoke tests pass (10 sec)
Tulio A M Mendes [Sat, 14 Mar 2026 02:00:57 +0000 (23:00 -0300)]
fix: rewrite toolchain patching to use sed instead of fragile unified diffs
The original patch files (binutils-adros.patch, gcc-adros.patch,
newlib-adros.patch) used hardcoded line numbers that didn't match the
actual source tarballs (binutils 2.42, gcc 13.2.0, newlib 4.4.0),
causing config.sub and ld/configure.tgt hunks to fail.
Replaced the entire patching approach with pattern-based sed commands
that match unique context strings instead of line numbers. This is
robust against upstream line shifts across versions.
Changes to toolchain/build.sh:
- patch_config_sub(): inserts adros case before pikeos*) and adds
adros* to the OS validation list next to dicos*
- patch_binutils(): sed-patches bfd/config.bfd, gas/configure.tgt,
ld/configure.tgt using pattern anchors
- patch_gcc(): sed-patches gcc/config.gcc, libgcc/config.host;
creates adros.h, crti-adros.S, crtn-adros.S inline
- patch_newlib(): sed-patches newlib/configure.host, sys/config.h,
libgloss/configure.ac; creates sys/adros/ and libgloss/adros/
files inline
Phase 5 — Proper malloc with free():
- Replaced bump allocator with address-ordered free-list allocator
- 8-byte aligned blocks with 8-byte header (size | used_bit, next_free)
- First-fit allocation with block splitting
- free() with address-ordered insertion and bidirectional coalescing
- realloc() now preserves old data size from block header
Tulio A M Mendes [Sat, 14 Mar 2026 00:18:08 +0000 (21:18 -0300)]
feat: gettimeofday + mprotect syscalls + Newlib libgloss port
Kernel:
- Add SYSCALL_GETTIMEOFDAY (127): returns RTC epoch seconds + TSC-derived
microseconds via struct timeval. Timezone arg ignored per POSIX.
- Add SYSCALL_MPROTECT (128): changes page protection on heap, mmap, and
stack regions. Converts POSIX PROT_READ/WRITE/EXEC to VMM flags and
calls vmm_protect_range(). Validates ownership before modifying PTEs.
ulibc:
- Add gettimeofday() wrapper in sys/time.h + time.c
- Add mprotect() wrapper in sys/mman.h + mman.c
- Add SYS_GETTIMEOFDAY/SYS_MPROTECT to ulibc syscall.h
Newlib port (newlib/):
- newlib/libgloss/adros/crt0.S: C runtime startup for AdrOS, calls
__libc_init_array/__libc_fini_array for Newlib/C++ ctor/dtor support
- newlib/libgloss/adros/syscalls.c: all 21 Newlib-required OS stubs
(_exit, _read, _write, _open, _close, _lseek, _fstat, _stat, _isatty,
_kill, _getpid, _sbrk, _link, _unlink, _fork, _execve, _wait, _times,
_gettimeofday, _rename, _mkdir) implemented via INT 0x80
- newlib/libgloss/adros/Makefile: builds crt0.o + libadros.a
- newlib/README.md: build instructions for full Newlib cross-compilation
- newlib/patches/README.md: documents config.sub, configure.host, and
libgloss/configure.in changes needed in Newlib source tree
All 21 Newlib libgloss stubs are now implemented. To build Newlib:
1. Copy libgloss/adros/ into Newlib source tree
2. Add i686-*-adros* target to configure files
3. Build with: ../configure --target=i686-adros && make
Tulio A M Mendes [Sat, 14 Mar 2026 00:02:44 +0000 (21:02 -0300)]
docs: full POSIX/Unix audit + fix git-clone breakage with submodules
- Add docs/FULL_POSIX_AUDIT.md: comprehensive analysis of POSIX gaps,
build system issues, and porting requirements for Newlib/GCC/Binutils/
Bash/Busybox
- Convert lwIP and doomgeneric from untracked nested repos to proper
git submodules (.gitmodules) so git clone --recursive works
- Update .gitignore: remove third_party/ and user/doom/doomgeneric
exclusions now that they are tracked as submodules
Build system was broken after git clone: lwIP (required) and doomgeneric
(optional) directories were empty because they were gitignored nested
repos with no .gitmodules. Now users can:
git clone --recursive <url>
make CROSS=1 && make iso
Tulio A M Mendes [Fri, 13 Mar 2026 23:45:00 +0000 (20:45 -0300)]
fix: build system cleanup — replace non-freestanding <string.h> with utils.h, parameterize toolchain, fix header conflicts
- Kernel: replace #include <string.h> (not freestanding) with #include "utils.h"
in src/arch/x86/fpu.c, src/kernel/fpu.c, src/rump/rumpuser_adros.c
- Makefile: define USER_CC/USER_LD/USER_AR variables for userspace cross-compilation,
replacing all hardcoded i686-elf-gcc/ld references in FULLTEST, DYN_CC, DYN_LD,
LDSO, PIE_SO, PIE_ELF rules
- Makefile: pass USER_CC/USER_LD/USER_AR to ulibc sub-make for consistent toolchain
- Makefile: add missing .PHONY targets (test-battery, run-arm, run-riscv, run-mips)
- ulibc Makefile: use ?= for CC/AS/AR/LD so parent can override
- doom Makefile: use ?= for CC/LD so parent can override
- ulibc: remove duplicate stat/fstat declarations from unistd.h (conflicted with
sys/stat.h when both included); proper declarations remain in sys/stat.h per POSIX
Tulio A M Mendes [Tue, 17 Feb 2026 08:01:55 +0000 (05:01 -0300)]
feat: /dev/hdX block device nodes and /proc/dmesg
1. ATA block devices in devfs:
Detected ATA drives (hda, hdb, hdc, hdd) are now registered as
block device nodes in /dev via ata_register_devfs(). Each node
supports read/write at byte offsets (with sector-aligned I/O
internally). Previously, drives were detected but invisible
in /dev, so 'ls /dev' showed no disk devices and mount had to
use ata_name_to_drive() internally.
2. /proc/dmesg:
Added /proc/dmesg to procfs that reads the kprintf ring buffer
via klog_read(). Uses heap allocation (16KB) to avoid kernel
stack overflow. The 'dmesg' command now works correctly.
Tulio A M Mendes [Tue, 17 Feb 2026 07:51:33 +0000 (04:51 -0300)]
feat: ALT key support in PS/2 keyboard driver
Added alt_held state tracking for Left ALT (scancode 0x38/0xB8)
and Right ALT (E0 0x38/0xB8). When ALT is held, the driver
emits ESC (0x1B) prefix before the character, which is the
standard terminal encoding for Alt+key combinations.
Also handles Right CTRL (E0 0x1D/0x9D) which was previously
ignored since it uses E0-prefixed scancodes.
Summary of modifier keys now supported:
- Shift (L/R): uppercase and symbol characters
- CTRL (L/R): control characters (c & 0x1F)
- ALT (L/R): ESC prefix + character
Tulio A M Mendes [Tue, 17 Feb 2026 07:47:48 +0000 (04:47 -0300)]
fix: mount command -t option argument parsing
The mount command parsed device=argv[1] and mountpoint=argv[2]
BEFORE scanning for -t, so 'mount -t fat /dev/hdb /test' set
device='-t' and mountpoint='fat' instead of the correct values.
Rewrote argument parsing to scan for -t first, then collect
remaining positional arguments as device and mountpoint.
Added usage message for missing arguments.
Tulio A M Mendes [Tue, 17 Feb 2026 07:45:06 +0000 (04:45 -0300)]
fix: keyboard CTRL key support for CTRL+C/CTRL+Z in shell
Root cause: The PS/2 keyboard driver had no CTRL key tracking.
When CTRL was held and a letter pressed, the driver sent the
plain letter character (e.g., 'c') instead of the control
character (e.g., 0x03 for CTRL+C).
Fix: Added ctrl_held state tracking in hal/x86/keyboard.c for
Left CTRL (scancode 0x1D press / 0x9D release). When CTRL is
held and a letter key is pressed, emit (c & 0x1F) — the
standard control character encoding:
CTRL+C = 0x03, CTRL+Z = 0x1A, CTRL+D = 0x04, etc.
Shell read_line_edit() already handled these bytes correctly:
- 0x03 (CTRL+C): prints ^C, newline, cancels current line
- 0x1A (CTRL+Z): ignored at prompt (no foreground job)
- 0x04 (CTRL+D): EOF on empty line
During command execution, tty_restore() re-enables ISIG, so
the TTY kernel driver intercepts CTRL+C/CTRL+Z and sends
SIGINT/SIGTSTP to the foreground process group via TIOCSPGRP.
Tulio A M Mendes [Tue, 17 Feb 2026 07:30:53 +0000 (04:30 -0300)]
fix: CTRL+C/CTRL+Z job control and doom build errors
1. CTRL+C/CTRL+Z: Shell now calls setsid() instead of setpgid(0,0)
to create a proper session. This initializes tty_session_id so
TIOCSPGRP can actually set child processes as the foreground
group. Previously, TIOCSPGRP silently returned -EPERM because
tty_session_id was 0.
2. Doom mkdir: Added mkdir/stat/fstat/chmod declarations to
user/ulibc/include/sys/stat.h where POSIX expects them.
Doom's m_misc.c includes sys/stat.h for mkdir().
3. Doom __divdi3: Added libgcc.a to doom link step to provide
compiler runtime helpers for 64-bit arithmetic on i386.
Tulio A M Mendes [Tue, 17 Feb 2026 07:15:18 +0000 (04:15 -0300)]
feat: shell job control (&, &&, ||) and CTRL+C/CTRL+Z support
1. Background processes (&): trailing & forks without waiting, prints
[bg] PID. Works for simple commands and pipelines.
2. Command chaining (&&): executes next command only if previous
succeeded (exit status 0). Skips remaining && chain on failure
until a || or ; is found.
3. OR chaining (||): executes next command only if previous failed
(exit status != 0). Skips remaining || chain on success until
a && or ; is found.
4. CTRL+C / CTRL+Z: shell ignores SIGINT/SIGTSTP/SIGQUIT. Child
processes get their own process group (setpgid) and are set as
the foreground group (TIOCSPGRP). CTRL+C sends SIGINT only to
the child, not the shell. After child exits, shell restores
itself as foreground group.
New files:
- user/ulibc/include/sys/wait.h: WIFEXITED/WIFSIGNALED/etc macros
Modified:
- user/sh.c: process_line rewritten for ;/&&/||/& operators,
run_simple and run_pipeline use setpgid+TIOCSPGRP job control
- user/ulibc/include/termios.h: added TIOCSPGRP/TIOCGPGRP
Tulio A M Mendes [Tue, 17 Feb 2026 07:01:05 +0000 (04:01 -0300)]
fix: init PID 1, ls -l permissions/size, doom dynamic linking
1. Init process now gets PID 1 (like Linux): next_pid starts at 2,
sched_assign_pid1() explicitly assigns PID 1 to the init process
after it loads. Kernel threads and AP idles get PIDs 2+.
2. ls -l now shows permissions, nlink, size via stat() on each entry
instead of just the type character.
3. doom.elf Makefile switched from static linking (libulibc.a) to
dynamic linking (libc.so via ld.so) like all other user commands.
Tulio A M Mendes [Tue, 17 Feb 2026 06:50:42 +0000 (03:50 -0300)]
fix: diskfs kfree-on-static-root, mount syscall, user addr space 8MiB->1GiB
Bug 1: ls /disk heap corruption — diskfs_close_impl called kfree on
static g_root BSS variable. Added guard: skip kfree when node == g_root.
Bug 2: mount command only displayed mounts. Added SYSCALL_MOUNT (126)
with support for tmpfs and disk-based filesystems (diskfs/fat/ext2/persistfs).
Updated userspace mount to call the syscall with device, mountpoint, and
-t fstype args.
Bug 3: doom 'Unable to allocate 5 MiB' — user address space was capped
at 8 MiB (USER_STACK_BASE=0x00800000). Raised to 1 GiB (0x40000000) in
elf.c, usermode.c, and syscall_brk_impl.
- Fix echo: leading space when flags shift arg index
- Fix tail: off-by-one with trailing newline
- Fix tee/touch/cp/mv/dd: missing mode arg on open(O_CREAT)
- Fix ulibc open(): make variadic to accept optional mode
- Update smoke_test.exp with 8 new patterns (97 total)
- Add host utility tests to Makefile test-host target
Tulio A M Mendes [Tue, 17 Feb 2026 04:31:30 +0000 (01:31 -0300)]
fix: shell/command bugs, new utilities, procfs race condition
Shell fixes:
- Fix DELETE key showing ~ (handle \x1b[3~ escape sequence + Home/End)
- Fix builtin redirections (echo > file now works via saved fd restore)
- Fix initrd readdir (root cause of ls /bin empty + tab completion broken)
Command fixes:
- Fix cut -dX/-fN combined argument parsing (POSIX style)
- Fix ps showing ? for PIDs: add cmdline[128] to process struct, populate in execve + init
- Fix procfs race condition: use sched_lock for process list traversal
- Make sched_lock non-static for procfs access
New commands (22 total):
- Previous session: mount, umount, env, kill, sleep, clear, ps, df, free, tee, basename, dirname, rmdir
- This session: grep, id, uname, dmesg, printenv, tr, dd, pwd, stat
Arch contamination note: vdso.c includes arch/x86/kernel_va_map.h directly (acceptable for now, only x86 target)
Tulio A M Mendes [Tue, 17 Feb 2026 00:45:26 +0000 (21:45 -0300)]
fix: KVA_IOAPIC VA collision with BSS — move from 0xC0201000 to 0xC0401000
Root cause: multiboot_copy (64KB static buffer) starts at VA 0xC0200FE0,
spanning pages 0xC0200000-0xC0210000. KVA_IOAPIC at 0xC0201000 mapped
IOAPIC MMIO over the BSS page containing the multiboot2 cmdline tag data.
After arch_platform_setup, reading bi->cmdline returned IOAPIC register
data (zeros) instead of the original cmdline string.
Symptom: [CMDLINE] "" regardless of GRUB menu entry selected.
Classic Heisenbug — adding a debug kprintf before IOAPIC init read the
correct data, masking the corruption.
Fix: move KVA_IOAPIC to 0xC0401000 (next to LAPIC at 0xC0400000),
well past _end at 0xC0265728. Updated VA map comment to reflect
current BSS extent (~0xC0266000).
4 fixes for VirtualBox compatibility + 1 cosmetic:
1. UART hardware detection (fixes boot freeze with serial disabled)
- hal_uart_init() now probes the scratch register before configuring
- All UART operations (putc, drain_rx, poll_rx, try_getc) guarded
behind uart_present flag — prevents infinite loop on floating bus
- console_init() auto-enables VGA when no UART detected so boot
messages are visible
- Added hal_uart_is_present() API + stubs for ARM/MIPS/RISC-V
2. alarm/SIGALRM test: replace 20M-iteration busy-loop with nanosleep
polling (50ms × 40 = 2s max wait). Fast VirtualBox CPUs completed
the busy-loop before the 1-second alarm fired.
3. x86_enter_usermode: load DS/ES/FS/GS=0x23 before iret to ring 3.
Without this, iret nulls segment registers (kernel DPL=0 < new CPL=3
per Intel SDM §6.12.1). On QEMU this was masked by early context
switches that fixed DS via x86_enter_usermode_regs, but VirtualBox
with Hyper-V acceleration may expose the race window.
4. User-mode exception handling: deliver SIGSEGV for any ring-3
exception (#GP, #UD, etc.) instead of kernel panic. Previously only
#PF (14) had this handling. A user-mode #GP now kills the process
cleanly instead of halting the entire system.
5. LAPIC timer ticks printed in decimal instead of hex.
Kernel fix — SMP orphan reparenting:
- process_exit_notify() hardcoded parent_pid=1 for reparenting, but with
SMP the AP idle processes consume PIDs 1-3 before the init userspace
process is created (PID 4+).
- Added sched_set_init_pid() to register the actual init process PID.
- arch_platform.c calls sched_set_init_pid(current_process->pid) before
entering userspace, so orphan reparenting targets the correct process.
Bug 1 — Fork FD race (HIGH severity):
process_fork_create() enqueued the child to the runqueue under
sched_lock, but syscall_fork_impl() copied file descriptors AFTER
the function returned — with sched_lock released. On SMP, the child
could be scheduled on another CPU and reach userspace before FDs
were populated, seeing NULL file descriptors.
Fix: move FD copying (with refcount bumps) into process_fork_create()
itself, under sched_lock, before the child is enqueued. Added proper
rollback of refcount bumps if kstack_alloc fails.
Bug 2 — Orphaned zombie leak (MEDIUM severity):
When a process exited, its children were not reparented to PID 1
(init). Zombie children of exited parents could never be reaped via
waitpid, leaking process structs and kernel stacks forever.
Fix: in process_exit_notify(), iterate the process list and reparent
all children to PID 1. If any reparented child is already a zombie
and init is blocked in waitpid(-1), wake init immediately.
Also verified (no bugs found):
- EOI handling correct (sent before handlers, spurious skips EOI)
- Lock ordering safe (all locks use irqsave, no cross-CPU ABBA)
- Heap has double-free and corruption detection
- User stack has guard pages
Tulio A M Mendes [Mon, 16 Feb 2026 22:24:18 +0000 (19:24 -0300)]
feat: LZ4 official Frame format for initrd compression/decompression
Replace custom 'LZ4B' block wrapper with the official LZ4 Frame format
(spec: https://github.com/lz4/lz4/blob/dev/doc/lz4_Frame_format.md).
Compressor (tools/mkinitrd.c):
- Write official frame: magic 0x184D2204, FLG/BD descriptor with
content size and content checksum flags, xxHash-32 header checksum,
data block, EndMark, xxHash-32 content checksum
- Fix block compressor MFLIMIT: last match must start >= 12 bytes
before end of block (was 5, violating spec)
Tulio A M Mendes [Mon, 16 Feb 2026 22:08:36 +0000 (19:08 -0300)]
fix: PMM total_memory overflow — MMAP reserved regions near 4GB inflated highest_addr
Root cause: Multiboot2 MMAP includes a BIOS reserved region at
0xFFFC0000-0x100000000. The end address (0x100000000) overflows
uint32_t when stored in a uint64_t local variable, and (unsigned)
truncation yields 0 — hence '[PMM] total_memory bytes: 0x0'.
Fixes:
- Use uint32_t locals (32-bit x86 caps RAM at 512 MB anyway)
- Clamp MMAP end addresses to 0xFFFFFFFF before comparison
- Only track highest_avail from AVAILABLE regions, not reserved
- Use 'if' instead of 'else if' so both BASIC_MEMINFO and MMAP
are processed in the same pass
- Print total_memory and freed_frames in decimal with MB suffix
Tulio A M Mendes [Mon, 16 Feb 2026 21:08:55 +0000 (18:08 -0300)]
feat: SMP load balancing for fork/clone + IPI resched
Enable load balancing in process_fork_create and process_clone_create:
both now dispatch to the least-loaded CPU via sched_pcpu_least_loaded().
All three process creation functions (create_kernel, fork, clone) now
send IPI_RESCHED to the target CPU after releasing sched_lock, waking
idle APs immediately when work is enqueued to their runqueue.
Tulio A M Mendes [Mon, 16 Feb 2026 21:04:47 +0000 (18:04 -0300)]
feat: SMP load balancing — per-CPU TSS, AP GDT reload, BSP-only timer work
Three fixes enable kernel thread dispatch to any CPU:
1. Per-CPU TSS (gdt.c, gdt.h): Replace single TSS with tss_array[SMP_MAX_CPUS].
Each AP gets its own TSS via tss_init_ap() so ring 3→0 transitions use
the correct per-task kernel stack on any CPU.
2. AP GDT virtual base reload (smp.c): The AP trampoline loads the GDT with
a physical base for real→protected mode. After paging is active, reload
the GDTR with the virtual base and flush all segment registers. Without
this, ring transitions on APs read GDT entries from the identity-mapped
physical address, causing silent failures for user-mode processes.
3. BSP-only timer work (timer.c): Gate tick increment, vdso update,
vga_flush, hal_uart_poll_rx, and process_wake_check to run only on
CPU 0. APs only call schedule(). Prevents non-atomic tick races,
concurrent VGA/UART access, and duplicate wake processing.
4. Per-CPU SYSENTER stacks (sysenter_init.c): Each AP gets its own
SYSENTER ESP MSR pointing to a dedicated stack.
5. Load balancing (scheduler.c): process_create_kernel dispatches to
the least-loaded CPU via sched_pcpu_least_loaded(). All CPUs update
their own TSS ESP0 during context switch.
- IPI vector 0xFD (253) registered in IDT + ISR assembly stub
- isr_handler dispatches vector 253: sends LAPIC EOI then calls
schedule() on the receiving CPU
- sched_ipi_resched() sends IPI to wake a remote idle CPU when
work is enqueued to its runqueue (avoids waking self)
- sched_enqueue_ready() sends IPI after enqueuing to remote CPU
- sched_pcpu_inc_load() called when enqueuing new kernel threads
All processes still dispatched to CPU 0 — per-CPU TSS is needed
before user processes can run on APs. The IPI + load tracking
infrastructure is ready for when per-CPU TSS is added.
Tulio A M Mendes [Mon, 16 Feb 2026 19:00:38 +0000 (16:00 -0300)]
feat: AP scheduler entry (SMP Phase 3)
Enable scheduling on Application Processors:
- Load IDT on APs via idt_load_ap() — root cause of AP crashes was
missing lidt, causing triple-fault when LAPIC timer fires
- Create per-CPU idle process for each AP in sched_ap_init()
- Start LAPIC timer on APs using BSP-calibrated ticks (no PIT
recalibration needed — all CPUs share same bus clock)
- AP timer handler calls schedule() for local CPU runqueue
- BSP signals APs via ap_sched_go flag after timer_init completes
- Allocations in sched_ap_init done outside sched_lock to avoid
ABBA deadlock with heap lock
- TSS updates restricted to CPU 0 (shared TSS, only BSP runs
user processes)
- AP stack increased to 8KB to match kernel thread stack size
All processes still assigned to CPU 0 — Phase 4 will add load
balancing to distribute processes across CPUs.
Tulio A M Mendes [Mon, 16 Feb 2026 18:26:26 +0000 (15:26 -0300)]
refactor: per-CPU runqueue data structure (SMP Phase 2)
Replace global rq_active/rq_expired with per-CPU runqueue array:
- struct cpu_rq: active/expired runqueue pair + idle process per CPU
- pcpu_rq[SCHED_MAX_CPUS] array replaces global runqueue pointers
- All enqueue/dequeue operations now index by process cpu_id field
- schedule() uses percpu_cpu_index() to select local CPU's runqueue
- process_init() initializes all CPU runqueues, sets pcpu_rq[0].idle
- Added cpu_id field to struct process (set to 0 for now)
- rq_pick_next() takes cpu parameter, swaps per-CPU active/expired
- All wake paths (kill, signal, sleep wake, exit_notify) enqueue
to the target process's assigned CPU runqueue
All processes still assigned to CPU 0 — Phase 3/4 will activate
AP scheduling and load balancing.
Tulio A M Mendes [Mon, 16 Feb 2026 18:17:26 +0000 (15:17 -0300)]
refactor: per-CPU current_process via GS segment (SMP Phase 1)
Replace the global current_process variable with per-CPU access
through the GS-based percpu_data structure on x86:
- process.h: #define current_process percpu_current() on x86,
keeps extern fallback for non-x86
- scheduler.c: write sites use percpu_set_current()
- interrupts.S: ISR entry now reloads percpu GS by reading LAPIC ID
from MMIO (0xC0400020) and looking up the correct GS selector in
_percpu_gs_lut[256] — solves the chicken-and-egg problem of
needing GS to find the CPU but GS being clobbered by user TLS
- percpu.c: _percpu_gs_lut lookup table populated during percpu_init()
- hal_cpu_set_tls: no longer loads GS immediately (would clobber
kernel percpu GS); user TLS GS is restored on ISR exit via pop
This is the foundation for running the scheduler on AP cores.
Tulio A M Mendes [Mon, 16 Feb 2026 18:03:36 +0000 (15:03 -0300)]
feat: USTAR+LZ4 compressed initrd
Add LZ4 block compression to the initrd pipeline:
- src/kernel/lz4.c + include/lz4.h: standalone LZ4 block decompressor
(~80 lines, no external dependencies)
- src/drivers/initrd.c: auto-detect LZ4B magic at boot, decompress
into heap buffer, then parse the contained USTAR tar as before
- tools/mkinitrd.c: built-in LZ4 block compressor (greedy hash-table),
builds tar in memory then wraps in LZ4B envelope
(magic + orig_size + comp_size + compressed data)
Format: LZ4B header (12 bytes) + raw LZ4 block. Falls back to
uncompressed tar if compression fails.
Results on current initrd (12 files including doom.elf):
TAR: 562 KB -> LZ4B: 326 KB (58% ratio)
Backward compatible: kernel still accepts plain USTAR tar
(no LZ4B magic = parse directly).
Tulio A M Mendes [Mon, 16 Feb 2026 17:47:10 +0000 (14:47 -0300)]
fix: replace pmm_alloc_page_low with pmm_alloc_page — fix fork OOM
The below-16MB page allocator (pmm_alloc_page_low) randomly sampled
pages and discarded any above 16MB. With 100 zombie children holding
CoW address spaces, the low-memory pool exhausted and fork() returned
-ENOMEM, killing init before the SIGSEGV/waitpid-100/echo.elf tests.
On 32-bit PAE all physical addresses are below 4GB, so the 16MB
restriction is unnecessary for PDPTs, page directories, page tables,
and user frames.
Changes:
- vmm.c: replace all pmm_alloc_page_low() with pmm_alloc_page(),
remove the dead pmm_alloc_page_low function
- usermode.c: replace pmm_alloc_page_low_16mb() with pmm_alloc_page(),
remove the dead function
- init.c: make SIGSEGV test failure non-fatal (goto instead of
sys_exit) so subsequent tests still run
Kernel (elf.c):
- Skip R_386_JMP_SLOT relocations when PT_INTERP present (let ld.so resolve lazily)
- Load DT_NEEDED shared libraries at SHLIB_BASE (0x11000000)
- Support ET_EXEC and ET_DYN interpreters with correct base offset
- Fix AT_PHDR auxv computation for PIE binaries
- Store auxv in static buffer for execve to push in correct stack position
- Use pmm_alloc_page() instead of restrictive low-16MB allocator
Execve (syscall.c):
- Push auxv entries right after envp[] (Linux stack layout convention)
so ld.so can find them by walking argc → argv[] → envp[] → auxv
ld.so (ldso.c):
- Complete rewrite for lazy PLT/GOT binding
- Parse auxv (AT_ENTRY, AT_PHDR, AT_PHNUM, AT_PHENT)
- Find PT_DYNAMIC, extract DT_PLTGOT/DT_JMPREL/DT_PLTRELSZ/DT_SYMTAB/DT_STRTAB
- Set GOT[1]=link_map, GOT[2]=_dl_runtime_resolve trampoline
- Implement _dl_runtime_resolve asm trampoline + dl_fixup C resolver
- Symbol lookup in shared library via DT_HASH at SHLIB_BASE
- Compiled as non-PIC ET_EXEC at INTERP_BASE (0x12000000)
VMM (vmm.c):
- Use pmm_alloc_page() for page table allocation (PAE PTs can be anywhere)
Test infrastructure:
- PIE test binary (pie_main.c) calls test_add() from libpietest.so via PLT
- Shared library (pie_func.c) provides test_add()
- Smoke test patterns for lazy PLT OK + PLT cached OK
- 80/83 smoke tests pass, cppcheck clean
Condition Variables (kcond_t):
- kcond_init/wait/signal/broadcast in sync.c
- kcond_wait atomically releases mutex, blocks, re-acquires on wakeup
- Supports timeout (ms) via PROCESS_SLEEPING + wake_at_tick
- Required by rumpuser for driver sleep/wake patterns
TSC-based Nanosecond Clock:
- TSC calibrated during LAPIC timer PIT measurement window
- clock_gettime_ns() returns nanoseconds since boot via rdtsc
- Falls back to tick-based 10ms granularity if TSC unavailable
- CLOCK_MONOTONIC syscall now uses nanosecond precision
- Linked against libgcc.a for 64-bit division on i386
Shared IRQ Handling (IRQ Chaining):
- Static pool of 32 irq_chain_node entries for shared vectors
- register_interrupt_handler auto-chains when vector already has handler
- unregister_interrupt_handler removes handler from chain
- isr_handler dispatches to all chained handlers for shared IRQs
- Transparent: single-handler fast path preserved (legacy slot)
- Required for PCI IRQ sharing and Rump Kernel driver integration
Tulio A M Mendes [Mon, 16 Feb 2026 00:45:17 +0000 (21:45 -0300)]
feat: FPU/SSE context save/restore for correct floating-point across context switches
- arch_fpu_init(): initialize x87 FPU (CR0.NE, clear EM/TS), enable OSFXSR if FXSR supported
- arch_fpu_save/restore: FXSAVE/FXRSTOR (or FSAVE/FRSTOR fallback) per process
- FPU state (512B) added to struct process, initialized for new processes
- fork/clone inherit parent FPU state; kernel threads get clean state
- schedule() saves prev FPU state before context_switch, restores next after
- Heap header padded 8->16 bytes for 16-byte aligned kmalloc (FXSAVE requirement)
- Added -mno-sse -mno-mmx to kernel ARCH_CFLAGS (prevent SSE in kernel code)
- Weak stubs in src/kernel/fpu.c for non-x86 architectures