--- /dev/null
+# AdrOS Audit Fix Plan
+
+Consolidated from the 2026-05-20 technical audit. Issues ordered by severity
+and exploitability. Each entry references the exact code location and proposed
+fix.
+
+---
+
+## Round 1 — CRITICAL: Kernel Memory Isolation & W^X
+
+### K01: mmap MAP_FIXED end address not validated
+
+**File**: `src/kernel/syscall.c:3040-3043`
+
+**Bug**: Only `addr` is checked against `kernel_base`. The end address
+`addr + aligned_len` can cross into kernel space, allowing a user process to
+overwrite kernel page tables.
+
+**Fix**: After the existing `addr >= kernel_base` check, add:
+```c
+if (addr + aligned_len > hal_mm_kernel_virt_base())
+ return (uintptr_t)-EINVAL;
+```
+Also validate that `addr + aligned_len` does not wrap around (overflow).
+
+---
+
+### K02: mprotect range crosses kernel boundary
+
+**File**: `src/kernel/syscall.c:4176-4222`
+
+**Bug**: The permissive stack check at line 4209 (`addr >= 0x08000000U &&
+addr < kern_base`) allows any range in that region, even if `addr + aligned_len`
+extends past `kern_base` into kernel space. `vmm_protect_range` would then
+modify kernel page flags.
+
+**Fix**:
+1. Add end-address validation: `if (addr + aligned_len > kern_base) return -ENOMEM;`
+2. Remove the permissive fallback or restrict it to only the actual stack
+ region (track stack bottom in `struct process`).
+
+---
+
+### K03: shm_at maps without address validation
+
+**File**: `src/kernel/shm.c:150-162`
+
+**Bug**: User-supplied `shmaddr` is used directly without checking alignment
+or kernel boundary. The auto-assigned address (`0x40000000U + mslot * ...`)
+can also overlap existing mappings or exceed user space.
+
+**Fix**:
+1. If `shmaddr != 0`, validate alignment (`shmaddr & 0xFFF == 0`) and
+ `shmaddr + seg->npages * PAGE_SIZE <= kernel_base`.
+2. For auto-assignment, use `vmm_find_free_area` instead of a fixed formula.
+3. Check that the range doesn't overlap existing mmap entries.
+
+---
+
+### A01: NX flag lost on COW clone and COW fault
+
+**File**: `src/arch/x86/vmm.c:446-459` (clone), `:505,525` (fault handler)
+
+**Bug**: `vmm_as_clone_user_cow` builds `new_pte` with only
+`PRESENT | USER | COW`, discarding the NX bit from the original PTE.
+`vmm_handle_cow_fault` maps the new page with `PRESENT | RW | USER`,
+also losing NX. This breaks W^X: executable+writable pages in the parent
+become writable+executable in the child after COW resolution.
+
+**Fix**:
+
+In `vmm_as_clone_user_cow` (line 446):
+```c
+uint64_t new_pte = (uint64_t)frame_phys | X86_PTE_PRESENT | X86_PTE_USER;
+if (pte & X86_PTE_RW) {
+ new_pte |= X86_PTE_COW;
+ // Preserve NX from original
+ if (pte & X86_PTE_NX) new_pte |= X86_PTE_NX;
+ src_pt[ti] = new_pte;
+ invlpg(va);
+} else {
+ new_pte = pte; // already preserves NX
+}
+```
+
+And in `vmm_as_map_page_nolock`, pass `VMM_FLAG_NX` when `new_pte & X86_PTE_NX`.
+
+In `vmm_handle_cow_fault` (lines 505 and 525):
+```c
+// Preserve NX from original PTE
+uint64_t nx = pte & X86_PTE_NX;
+// rc <= 1 path:
+pt[ti] = (uint64_t)old_frame | X86_PTE_PRESENT | X86_PTE_RW | X86_PTE_USER | nx;
+// copy path:
+pt[ti] = (uint64_t)(uintptr_t)new_frame | X86_PTE_PRESENT | X86_PTE_RW | X86_PTE_USER | nx;
+```
+
+---
+
+## Round 2 — HIGH: Permission & Access Control
+
+### A03: read/write ignore fd open mode
+
+**File**: `src/kernel/syscall.c:2740-2806` (read), `:2810-2861` (write)
+
+**Bug**: `syscall_read_impl` does not check if the fd was opened `O_WRONLY`,
+and `syscall_write_impl` does not check if the fd was opened `O_RDONLY`.
+
+**Fix**:
+
+In `syscall_read_impl`, after `fd_get`:
+```c
+if ((f->flags & 3U) == 1U) return -EBADF; /* O_WRONLY */
+```
+
+In `syscall_write_impl`, after `fd_get`:
+```c
+if ((f->flags & 3U) == 0U) return -EBADF; /* O_RDONLY */
+```
+
+---
+
+### A06: kill syscall lacks permission checks
+
+**File**: `src/kernel/scheduler.c:365-424`
+
+**Bug**: `process_kill` sends signals to any process without checking
+caller credentials. Any user can kill any other user's processes (or root's).
+
+**Fix**: Add permission check in `process_kill` before setting
+`sig_pending_mask`:
+```c
+/* Permission check (POSIX): sender must be root, or same uid, or same euid */
+if (current_process && current_process->euid != 0) {
+ if (current_process->euid != p->uid && current_process->uid != p->uid)
+ return -EPERM;
+}
+/* SIGCONT is special: can always be sent if same session (skip for now) */
+```
+Same check needed in `process_kill_pgrp`.
+
+---
+
+### SYSENTER: user stack pointer not validated
+
+**File**: `src/arch/x86/sysenter.S:84-85`
+
+**Bug**: `mov 4(%ecx), %edx` and `mov 8(%ecx), %ecx` read from the user ESP
+without validating that ECX points to user space. A malicious ECX pointing
+into kernel memory leaks kernel data into syscall arguments.
+
+**Fix**: Add validation in the assembly entry or in `syscall_handler`:
+- In `sysenter_entry`, after pushing the iret frame, check that ECX is
+ below `KERNEL_VIRT_BASE` before dereferencing. If not, set ECX/EDX to 0
+ and set EAX to -EFAULT.
+- Alternatively, validate in C before the `mov 4(%ecx)` / `mov 8(%ecx)`
+ by using `copy_from_user` to read arg2/arg3 from the saved user ESP
+ in the registers struct.
+
+---
+
+### AIO: passes user buffer directly to VFS
+
+**File**: `src/kernel/syscall.c:1656-1666`
+
+**Bug**: `syscall_aio_rw_impl` passes `cb.aio_buf` (a user-space pointer)
+directly to `f->node->f_ops->read/write`. The VFS function then reads/writes
+from/to user memory without `copy_from_user`/`copy_to_user`, bypassing SMAP.
+
+**Fix**: Allocate a kernel bounce buffer, `copy_from_user` into it (for
+writes), call VFS with the kernel buffer, then `copy_to_user` (for reads).
+
+---
+
+### Socket send/recv: passes user buffer directly to lwIP
+
+**File**: `src/kernel/syscall.c:4695,4711` → `src/kernel/socket.c:377-432`
+
+**Bug**: `ksocket_send` passes the user buffer directly to `tcp_write` and
+`memcpy(p->payload, buf, len)`. `ksocket_recv` writes directly to the user
+buffer via `rxbuf_read`. With SMAP enabled, the `user_range_ok` check is
+done but the actual access bypasses `copy_from_user`/`copy_to_user`.
+
+**Fix**:
+- For send: `copy_from_user` into a kernel buffer, then pass to lwIP.
+- For recv: `rxbuf_read` into a kernel buffer, then `copy_to_user`.
+
+---
+
+### vDSO tick_hz mismatch
+
+**File**: `src/kernel/vdso.c:35` vs `include/timer.h:15`
+
+**Bug**: `vdso_kptr->tick_hz = 50` but `TIMER_HZ = 100`. User-space time
+calculations using vDSO will be off by 2x.
+
+**Fix**: Replace hardcoded 50 with `TIMER_HZ`:
+```c
+vdso_kptr->tick_hz = TIMER_HZ;
+```
+Add `#include "timer.h"` to `vdso.c`.
+
+---
+
+## Round 3 — MEDIUM: POSIX Compliance & Robustness
+
+### truncate/ftruncate: no write permission check
+
+**File**: `src/kernel/syscall.c:4299-4323`
+
+**Bug**: Neither syscall checks that the fd was opened for writing, or that
+the caller has write permission on the file.
+
+**Fix**:
+- `ftruncate`: check `(f->flags & 3U) == 0U` (O_RDONLY) → return -EBADF.
+- `truncate`: `vfs_check_permission(node, 2)` (write) → return -EACCES.
+
+---
+
+### O_EXCL not enforced
+
+**File**: `src/kernel/syscall.c:2361-2365`
+
+**Bug**: When `O_CREAT | O_EXCL` is specified and the file already exists,
+the open should fail with `-EEXIST`. Currently it succeeds (falls through
+to the O_TRUNC check or just opens normally).
+
+**Fix**: After `vfs_lookup` finds the node:
+```c
+if (node && (flags & 0x40U) && (flags & 0x80U)) /* O_CREAT | O_EXCL */
+ return -EEXIST;
+```
+
+---
+
+### O_NOFOLLOW not enforced
+
+**File**: `src/kernel/syscall.c:2341-2404`
+
+**Bug**: `O_NOFOLLOW` (0x20000) should cause open to fail with `-ELOOP`
+if the path refers to a symlink. AdrOS doesn't have symlinks yet, so this
+is a no-op for now but the flag should be accepted silently.
+
+**Status**: Deferred (no symlink support yet).
+
+---
+
+### O_DIRECTORY not enforced
+
+**File**: `src/kernel/syscall.c:2341-2404`
+
+**Bug**: `O_DIRECTORY` (0x10000) should fail with `-ENOTDIR` if the path
+is not a directory.
+
+**Fix**: After `vfs_lookup`:
+```c
+if ((flags & 0x10000U) && node && !(node->flags & FS_DIRECTORY))
+ return -ENOTDIR;
+```
+
+---
+
+### posix_spawn wrapper broken
+
+**File**: `user/ulibc/src/spawn.c:22`
+
+**Bug**: Uses `_syscall2(SYS_POSIX_SPAWN, path, argv)` but the kernel
+handler expects 4 arguments (pid_out, path, argv, envp). The missing
+arguments cause the child to exec with garbage envp, and the parent
+doesn't get the child PID.
+
+**Fix**: Change to `_syscall4`:
+```c
+int ret = _syscall4(SYS_POSIX_SPAWN, (int)pid, (int)path, (int)argv, (int)envp);
+```
+
+---
+
+### SYSCALL_MKDIR ignores mode argument
+
+**File**: `src/kernel/syscall.c:3693-3696`
+
+**Bug**: `syscall_mkdir_impl` doesn't receive or pass the mode argument.
+
+**Fix**: Read mode from `sc_arg1(regs)` and pass it through to `vfs_mkdir`.
+Update `vfs_mkdir` signature to accept mode_t.
+
+---
+
+### CLONE_VM address space refcount missing
+
+**File**: `src/kernel/scheduler.c:772-774,338-344`
+
+**Bug**: When `CLONE_VM` is set, the child shares the parent's `addr_space`
+pointer. When any thread exits, `process_reap` checks `PROCESS_FLAG_THREAD`
+and skips `vmm_as_destroy`. But if the thread group leader exits before
+other threads, its `process_reap` destroys the address space while other
+threads still reference it.
+
+**Fix**: Add a `uint32_t as_refcount` to `struct process` (or a separate
+refcount table). Increment on CLONE_VM, decrement on reap. Only destroy
+when refcount reaches 0.
+
+---
+
+## Round 4 — LOW: Hardening & Cleanup
+
+### Saved set-user-ID not implemented
+
+**Bug**: `setuid`/`seteuid` don't maintain the POSIX saved set-user-ID.
+After `seteuid(uid)`, there's no way back without being root.
+
+**Fix**: Add `suid`/`sgid` fields to `struct process`. On `setuid`/`seteuid`,
+save the old euid to suid. Allow `seteuid(suid)` without root.
+
+---
+
+### No kernel reboot() syscall
+
+**Bug**: `init` handles SIGUSR1/SIGUSR2 by killing all processes and
+calling `_exit(0)`, but the kernel never actually reboots or powers off.
+
+**Fix**: Add `SYSCALL_REBOOT` that calls ACPI shutdown or keyboard
+controller reset.
+
+---
+
+### socket_syscall_dispatch misnamed
+
+**File**: `src/kernel/syscall.c`
+
+**Bug**: `socket_syscall_dispatch` handles MQ, SEM, DLOPEN, EPOLL, INOTIFY,
+AIO, MOUNT, PIVOT_ROOT, etc. — not just sockets.
+
+**Fix**: Rename to `extended_syscall_dispatch`.
+
+---
+
+## Test Plan
+
+After each round, run:
+```bash
+make iso && rm -f serial.log && make test # QEMU smoke
+make test-battery # Extended
+make test-host # Host unit tests
+cppcheck --enable=all --suppress=unusedFunction \
+ --suppress=missingIncludeSystem \
+ -I include -I user/ulibc/include src/ user/ # Static analysis
+```
+
+### New regression tests to add:
+
+| Test | Validates |
+|------|-----------|
+| mmap MAP_FIXED crossing kernel base | K01 |
+| mprotect range crossing kernel base | K02 |
+| shmat with kernel-space address | K03 |
+| fork preserves NX on code pages | A01 |
+| read on O_WRONLY fd returns EBADF | A03 |
+| write on O_RDONLY fd returns EBADF | A03 |
+| kill from non-root to root returns EPERM | A06 |
+| O_CREAT+O_EXCL on existing file returns EEXIST | O_EXCL |
+| ftruncate on O_RDONLY fd returns EBADF | truncate |
+| posix_spawn returns child PID and execs | posix_spawn |