Review findings tracker
One place for every external-review finding and its state, so none is lost between sessions. Updated whenever a finding arrives, is verified, or closes.
Read the key first. Every finding here has one ID in one sequence, whatever
review it came from: H- for high, M- for medium, numbered in the order they
arrived. The closed reviews before the eighth keep their own files and numbers.
Status words: fixed (commit named, red test first unless the row says why not), in progress, verified-open (confirmed against the code, not yet fixed), false (checked and refuted - evidence linked).
Earlier reviews are closed and written up: the sixth (review_sixth_verified.md), the seventh (review_seventh_verified.md) and those before (external_reviews_verified.md).
Eighth review (2026-09-10 onward, arriving one finding at a time)
| ID | Sev | Finding | Status | Evidence / next step |
|---|---|---|---|---|
| M-001 | M | brk shrink never unmapped | fixed 1dc5b67 | mm_brk_mmap_leaks.md |
| M-002 | M | mmap failure left pages mapped (both loops) | fixed 1dc5b67 + C5 | second loop found later; same file |
| H-003 | H | futex validates a user pointer, then reads it after a spinning lock | fixed (commit "core: a user copy that faults returns an error instead of panicking") | assembly copy with a recovery point in the trap handler; futex read and the exit join-word write use it. uaccess_recovery_open.md |
| H-004 | H | two region-list heads into one pool across threads | fixed ffa8463 | core_c5_process_state.md |
| H-005 | H | exit_group ended only the calling thread | fixed ffa8463 | same file |
| M-003 (a) | M | signal dispositions copied per thread | fixed ffa8463 | same file |
| M-003 (b) | M | pipe write tests readers outside g_pipe_lock; read half worse | fixed (commit "core: a pipe decides end of file under the lock that holds its data") | both decisions moved into the critical section with the data they depend on. No red test: the windows are a few instructions between two cores. pipe_eof_race_open.md |
| H-006 | H | exec from a thread left siblings running and the wrong id | fixed 8a3903c | THREADS_C5_EXEC, red first |
| M-004 | M | tkill/tgkill could not reach non-leader threads | fixed d6d6b1b | reported again later; already closed |
| H-007 | H | pid resolved to a slot index, used unlocked (ABA); lookups matched slots being built | fixed da50ce6 | no red test: the window does not reproduce in a boot; stated in the commit |
| M-005 | M | TCP RST accepted without a sequence check; also closed listeners | fixed b4a65ad | RFC 5961 3.2; host test red first |
| M-006 | M | mmap len + 0xFFF wraps to zero pages; munmap/mprotect aligned end wraps | fixed a40aab0 | red in the ABI self-test, split-verified |
| H-008 | H | TCP ISN = clock (0x1000 + now_ms, listen 0x2000 + now_ms) | fixed (commit "net: TCP initial sequence numbers come from a secret") | RFC 6528 with SipHash over one stack secret; host test red first. The secret is weak under QEMU TCG: no RDRAND, so it comes from the TSC mix and the boot log says so. Real entropy is its own open item |
| H-009 | H | DHCP: predictable xid; ACK not bound to the chosen server, offer or chaddr; renewals from anyone | fixed (commit "net: a DHCP reply belongs to this client's transaction with its server") | xid from the stack secret; server port, chaddr, server id; ACK only from the chosen server for the offered address, renewals for the address in use; a NAK from anyone used to drop the lease and now must come from the server. Red twice (as written, and with the xid check off). Not closable by a client: an on-segment attacker answering the broadcast DISCOVER first |
| M-007 | M | DNS: predictable id, fixed source port 0xC353, no server/port/question check | fixed (commit "net: a DNS answer is believed only if it answers the query sent") | server, port 53, response bit, id and the single question checked; id and port from the stack secret per query. Host test red first - the first version was red for the wrong reason (it captured an ARP request) and was corrected and re-proved red at the right step |
| H-010 | H | read() on a pipe, recv(), recvfrom() validate the buffer, block, then write it after wake-up; a sibling's munmap makes it a ring-0 fault -> panic | fixed (commit "core: a user copy that faults returns an error instead of panicking") | hw_pipe_read (write inside the blocking loop), hw_net_recv, hw_sys_recvfrom; also the console read (same shape, not in the report). Fixed with H-003, same commit: pipe read/write, console read, recv/recvfrom/send/sendto through the fault-tolerant copy (socket data via a kernel bounce buffer). Boot probe asserted by the gate; sabotage of the recovery branch turns it red |
| M-008 | M | any ARP overwrites the cache entry, gateway included | fixed (commit "net: a known neighbour's hardware address changes only by a reply the stack asked for") | an entry changes only through a reply to the request the stack sent; a request for this host may add, never change; stale entries are used and refreshed by a request. Host test red first. ARP has no authentication: racing the real reply to a request is still possible |
| M-009 | M | exFAT contiguous file: first + index wraps in 32 bits to a valid cluster; first_cluster from the entry never range-checked | fixed (commit "fs: cluster, run and length arithmetic from a volume cannot wrap") | exfat_nth_cluster (return first + index), parse takes rd32(stream + 20) as is, exfat_read_cluster checks only the wrapped value. Also, not in the report: read_at truncates (offset + done) / cluster_bytes to 32 bits, so a large declared size wraps the index even with a valid first cluster. Host exFAT tests exist, so the red test is deterministic |
| audit | M | exFAT, ext2, ISO9660 and NTFS read_at: offset + len > node->size after offset < size | fixed (commit "fs: cluster, run and length arithmetic from a volume cannot wrap") | found by the H-011 audit. Reachable only with a size declared near 2^64 and an offset within 4 GiB of it; then len is not trimmed and the read runs past the declared end (in exFAT, straight into M-009's index truncation). Fix with M-009/M-010: len > size - offset |
| M-010 | M | NTFS run-list: len_size/off_size up to 15 bytes, shifted << (8 * i) into 64-bit types - undefined behaviour | fixed (commit "fs: cluster, run and length arithmetic from a volume cannot wrap") | ntfs_next_run checks only that the bytes fit. More than reported: the sign extension -((int64_t)1 << (8 * off_size)) is already undefined at off_size == 8, the largest legal size; shifting a byte into the sign bit of int64_t is undefined too. The reviewer's second candidate is also real: vcn < seen + run.length overflows on a crafted length |
| H-011 | H | NTFS off + attr_len > size wraps in 32 bits, so a huge resident $DATA value is accepted and read_at copies past rec[] - kernel stack contents to user space | fixed (commit "fs: an NTFS attribute length cannot open its record up") | bounds are len > size - off after off <= size in ntfs_find_attr and ntfs_data_attr; host test red first. Audit of every parser done: the 8/16-bit record walks (ext2, ISO9660, NTFS index and run headers) cannot wrap; the four read paths below share a weaker form of the same pattern, recorded as its own row |
| H-012 | H | FAT: a first cluster from a directory entry is never range-checked; fat_cluster_lba is 32-bit and nothing bounds a sector by the partition - reads of other partitions' sectors | fixed (commit "fs: a FAT cluster from the disk names a sector of its own partition") - worse than reported, and the write half fixed too | fat_cluster_lba (data_lba + (cluster - 2) * spc, uint32) is checked nowhere; only fat_next_cluster checks max_clusters. The partition's total sectors is a local of the mount, never stored. Writes too: a parent directory's cluster comes from fat_resolve unchecked, fat_dir_sector turns it into a sector with fat_cluster_lba, and create, unlink and mkdir rewrite that sector with one 32-byte entry changed - a crafted subdirectory turns "create a file here" into a write to a sector of another partition. Data writes use freshly allocated clusters and are not affected. Fixed in vibeos_fat_cluster_sector (host-tested, red first): cluster range, 64-bit sector, partition bound; fat_cluster_lba returns 0 for a refused cluster and every read, list, directory-sector and write caller stops on it. Twelve boots 12/12 |
| H-013 | H | ext2: inode block pointers never checked against the volume; blocks_count not kept after mount, so a crafted pointer reads sectors past the partition | fixed (commit "fs: an ext2 block number names a block of its own volume") | ext2_read_block computed part_lba + block * sectors with no bound; ext2_map_block takes direct and indirect pointers as they come. Also, not in the report: ext2_read_inode takes the inode table block from the group descriptor unchecked, so a lookup alone can read past the volume. Fixed in the same place: the mount keeps blocks_count and ext2_read_block refuses any block at or past it. Host test red first, with a volume declared smaller than its device - an image whose volume fills the device would fail at the device and pass for the wrong reason |
| H-014 | H | TLB shootdown timeout returns and the caller proceeds, so a permission restriction "succeeds" while a target core may still hold the old translation | already fixed - the finding read stale code (41fd9af) | the finding quotes the timeout body (store 0; tlb_timeouts++; hw_log(...)) and stops one line early: it is immediately followed by hw_panic("TLB shootdown timed out: a core still holds a translation the caller has already revoked"). There is one shootdown implementation (hw_tlb_shootdown, the g_be.shootdown hook), and on timeout it fail-stops - exactly the finding's recommended fix. An earlier review found the old "return and continue" behaviour; the comment there records the whole reasoning. No live defect |
| H-015 | H | TLB quarantine overflow (>512 parked frames) falls back to an immediate vibeos_frame_put - the old racy release - so a frame can be recycled while another core holds a stale TLB entry; a single process can force it by munmapping >512 not-yet-quiescent pages | fixed (leak on overflow; dynamic quarantine is the follow-up) | hw_tlbq_put: the fallback is labelled "status quo: the old, racy release". The boot gate asserts tlbq_overflow=0 so CI catches it, but that is not a runtime guarantee. Fix: on overflow do a synchronous shootdown for that frame's address space then put, or apply backpressure until a slot is quiescent - not the racy put |
| H-016 | H | rt_sigaction and rt_sigprocmask dereference the user pointer directly after hw_user_range_ok (old/act read+write); a sibling thread's munmap between check and access faults in ring 0 | fixed (298b68d) | same class as H-003/H-010 but thread-race, no blocking. hw_sys_rt_sigaction (old_uptr writes, act reads), hw_sys_rt_sigprocmask (old_uptr write, set read). Fix: vibeos_uaccess_copy |
| H-017 | H | rt_sigaction stores the handler (act[0]) as sig_handler[sig] with no canonicality/user check; hw_signal_deliver puts it in frame->rip, and iretq to a non-canonical rip #GPs in ring 0 -> kernel panic, from ring 3 via rt_sigaction + kill | fixed (05fa793, red test) | canonical rip stays canonical, so validate at install: hw_user_addr_ok(handler) (rejects non-canonical and wrong-window). A canonical-but-unmapped handler faults in ring 3, which is safe. Red test possible: non-canonical handler + signal panics today |
| H-018 | H | rt_sigreturn reads the sigframe with a direct deref after hw_user_range_ok, and restores rip/rsp from the user-controlled frame without a canonicality check -> iretq #GP in ring 0 | fixed (read 298b68d, rip 05fa793) | cs/ss/rflags ARE normalised (no privilege escalation), so the live defect is the non-canonical rip/rsp -> iretq fault, plus the TOCTOU read. Fix: copy the frame via vibeos_uaccess_copy, validate restored.rip/rsp canonical+user, SIGSEGV otherwise |
| H-019 | H | hw_copy_user_string validates each byte then derefs it directly; a sibling munmap between the two faults in ring 0. Used by execve, open, unlink, mkdir, stat/readlink | fixed (298b68d) | int hw_copy_user_string(uptr,dst,max): hw_user_range_ok(uptr+i,1,0) then dst[i]=*(uptr+i). Fix: fault-safe per-byte vibeos_uaccess_copy, keep hw_user_range_ok as the address-policy check |
| H-020 | H | readv/writev validate the whole iovec array once, then deref v->len/v->base per element; a sibling munmap of the array page faults in ring 0 | fixed (298b68d) | hw_sys_writev/hw_sys_readv. Fix: copy each hw_iovec_t via vibeos_uaccess_copy into a kernel struct before reading base/len |
| M-017 | M | signal 64 is accepted (sig >= VIBEOS_HW_NSIG where NSIG=65) but 1ull << sig with sig=64 is undefined; the pending/blocked bitmask is 64-bit keyed by signal number, so signal 64 has no representable bit and can stay pending undelivered | fixed (278a803) | VIBEOS_HW_SIG_MAX 63u added; the five range checks that read sig >= VIBEOS_HW_NSIG now read sig > VIBEOS_HW_SIG_MAX, so a signal that has no representable bit is refused rather than accepted and lost. Red test: syscall(SYS_kill, getpid(), 64) expects EINVAL, gated in musl_signal.c (SIG_OK) |
| H-021 | H | clone() writes CLONE_PARENT_SETTID/CLONE_CHILD_SETTID through a user pointer after the range check; a sibling munmap faults in ring 0 | fixed (188ec0f) | hw_sys_clone_thread ptid/ctid stores. Same validate-then-use class; fix with vibeos_uaccess_copy, dropped on fault (the thread is created either way) |
| H-022 | H | waitpid() writes status_ptr directly after publishing the task FREE, releasing the lock and re-enabling interrupts; a sibling munmap between check and store faults in ring 0 | fixed (188ec0f) | hw_sys_waitpid. The reap is already complete, so a faulted write is dropped and the pid still returned, as Linux does after EFAULT there |
| H-023 | H | signal delivery builds the sigframe directly on the user stack after the range check (magic/blocked/frame and the return address); a sibling munmap faults in ring 0 | fixed | hw_signal_deliver. Built in a kernel buffer and copied out via vibeos_uaccess_copy; on fault no partial frame, task takes SIGSEGV |
| H-024 | H | arch_prctl(ARCH_GET_FS) writes t->fs_base to the user pointer after the range check | fixed | hw_sys_arch_prctl. vibeos_uaccess_copy |
| H-025 | H | ioctl TIOCGPGRP/TIOCSPGRP deref the user pointer directly after the check (write pgid, read pgid) | fixed | hw_sys_ioctl. vibeos_uaccess_copy both directions |
| H-026 | H | clock_gettime writes the timespec (ts[0], ts[1]) through a user pointer after the range check; a sibling munmap faults in ring 0 | fixed (with a proactive sweep of execve argv/poison, time, crash stack). Still direct: pipe2 fd write, hw_stat_wr (all stat paths) - needs a kernel-buffer stat + undo; follow-up | |
| H-027 | H | setsid() checks and assigns current->sid/pgid OUTSIDE g_sched_lock, then takes the lock only for the sibling-member updates; setpgid/getsid/kill read these under the lock, so a sibling races the transition | fixed | hw_sys_setsid: the pgid==tgid check and both current-> assignments are now under g_sched_lock with the sibling loop, so check+modify is one linearizable step; returns current->tgid to avoid a post-unlock read of a mutable field. No red test (thread-race, H-007 precedent); full check green, 3 boots pass |
| H-028 | H | TCP SYN_RECEIVED stores cs->parent = listener slot index; if the listener closes and the slot is reused, the final ACK delivers the child into the new socket backlog - no identity check | fixed | Each socket gets a monotonic gen at sock_alloc (net->sock_gen_seq); the child records the listener's gen as parent_gen at SYN time, and the final-ACK backlog insert now requires the parent slot to still be used && type==TCP && state==LISTEN && gen==parent_gen, else the child is dropped. Red test test_inet_tcp_accept_aba (close listener, new listener reuses the slot, drive the final ACK; the stranger's backlog must stay empty): fails on the used-only code, passes with the fix. Full check green, 3 boots pass |
| H-029 | H | mounted filesystems are not bounded by their partition: storage_scan drops sector_count, so ISO9660/exFAT read image-declared offsets/clusters as device LBAs and can read a neighbour partition | fixed (exFAT+ISO, red test; FAT/ext2 authoritative-bound upgrade is a follow-up) | root cause in storage.c (only first_lba kept). Fix: propagate sector_count, bound each read by [first_lba, first_lba+sector_count). NB my H-012/H-013 (FAT/ext2) bounded by the IMAGE-declared size, not the partition - not fully authoritative; upgrade them too (follow-up) |
| M-018 | M | newfstatat truncates node.size (64-bit) to uint32_t, so st_size is wrong (mod 2^32) for files > 4 GiB | fixed (no red: fstat via fd path still 32-bit, separate) | hw_sys_newfstatat uint32_t size -> hw_write_stat. Keep it uint64_t. Deterministic test (a node > 4 GiB) |
| M-019 | M | ISO9660 directory sector count (length + 2047) / 2048 wraps in uint32 (length near UINT32_MAX -> sectors 0), so a directory reads as empty and its files vanish | fixed (red test) | iso_dir_find and iso_op_list (duplicated). Use ((uint64_t)length + 2047) / 2048, centralize, and bound by the volume |
| M-020 | M | blocking socket syscalls (connect/accept/recvfrom) hold a raw hw_fd_t * across the wait loop and re-read f->net_sock; a sibling thread closing+reusing the FD slot makes the call act on a different socket - slot-index ABA in the FD table | fixed (reopened 2026-09-24 and closed: recvfrom had been missed) | hw_sock_stable(f, sock, gen) in linux_socket.c: capture socket index + the socket's gen before blocking, re-verify on every wake (fd still used, net_sock unchanged, socket gen unchanged), else EBADF. Counted at [NET] MUSTBEZERO sock_fd_aba (also sock_stale_parent, H-028) - the network module's C2 must-be-zero. Reopened by an external review: the fix reached connect, accept and the stream read (hw_net_recv) but not hw_sys_recvfrom, the datagram syscall, which still re-read f->net_sock on every pass of its wait while this row said fixed. It re-verifies now, and check-net-stable.py (in check.sh) makes the rule mechanical: every function in net.c that holds an hw_fd_t and calls hw_net_wait_tick() must call hw_sock_stable. net-sock-stable.txt: removing it from recvfrom or accept is red by name |
| M-022 | M | ext2 directory size can wrap and hide the whole directory. inode_size() builds a 64-bit size from i_size+i_dir_acl; ext2_dir_find() then does nblocks=(size+block_size-1)/block_size, which overflows to 0 for a size near UINT64_MAX -> no blocks scanned, every lookup fails. Same in ext2_op_list(). Value comes straight from the image; distinct from M-019 (that was ISO9660) | fixed | kernel/fs/ext2.c: inode_size() ~202-209; ext2_dir_find() nblocks ~219; ext2_op_list() ~378-380. Fix: validate dir size vs blocks_count*block_size at inode read; compute blocks without overflowing sum (size/block_size + (size%block_size!=0)); don't treat i_dir_acl as high-32 unless the feature level allows |
| M-023 | M | orphaned TCP child stays allocated forever when the listener is closed during the handshake. H-028's parent check (used/type/state/gen) correctly drops the child, but the else path never closes/frees it: it still sets ESTABLISHED and clears rto_deadline_ms, then just skips the backlog insert. Child was sock_alloc'd (owner_pid=0, no FD), and the reclaim timer only frees sockets with close_deadline_ms!=0 -> used=1 indefinitely; a later RST -> CLOSED but not freed. Repeatable -> socket table exhaustion. Distinct from H-028 (that was misdelivery) | fixed | kernel/net/inet.c: tcp_input() child->ESTABLISHED ~1602-1605, parent-fail branch ends with no cleanup ~1614-1619; sock_alloc() ~481-496; vibeos_inet_poll reclaim ~1848-1855; tcp_input_rst ~1464-1474. Fix: if parent fails validation, do NOT establish - close+free the child immediately (used=0, CLOSED, deadline 0); centralize tcp_abort_socket(). Add a red test (SYN->close listener->final ACK->slot freed, no table exhaustion) |
| H-030 | H | TCP half-open connections not reclaimed after retry exhaustion (remote DoS). Each SYN to a listener creates a SYN_RECEIVED child via sock_alloc(); when retries hit TCP_MAX_RETRIES the timer only sets state=CLOSED, reset=1, rto_deadline_ms=0 - never used=0 nor close_deadline_ms. Slot stays occupied. A remote attacker sends SYNs without completing the handshake, repeats to exhaust VIBEOS_INET_MAX_SOCKETS, blocking new TCP. No FD or listener-close needed (child is kernel-owned until accept). Distinct from M-023 | fixed | kernel/net/inet.c: child creation tcp_input() ~1564-1586; retry reclaim vibeos_inet_poll() ~1878-1900 (~1886-1890 sets CLOSED but doesn't free); sock_alloc reuses only when used==0 ~480-496; general reclaim needs close_deadline_ms!=0 ~1848-1855. Fix: on retry exhaustion abort+reclaim fully (used=0, CLOSED, timer 0, remove backlog refs); centralize tcp_abort_socket(). Test: MAX_SOCKETS incomplete SYNs, wait retry expiry, table returns |
| M-031 | M | accepted TCP socket never inherits process ownership, leaks at exit. The child from the listener has owner_pid=0 (sock_alloc zeroes it); accept() assigns it to the new FD but never calls vibeos_inet_socket_set_owner(). hw_task_exit() releases sockets by tgid via vibeos_inet_release_owner_sockets(), which misses this child (no owner); the FD cleanup only sets f->net_sock=-1, not closing the stack socket -> orphaned kernel resource until table exhaustion. Normal path: listener->accept->thread exit without close(). Distinct from M-021 | fixed | kernel/arch/x86_64/linux_socket.c: hw_sys_accept() t->fds[nfd].net_sock=child ~195 without ownership; kernel/net/inet.c sock_alloc ~481-496; arch_hw.c hw_task_exit ~5200-5221. Fix: after allocating the FD in accept(), set child owner_pid=t->tgid under g_net_lock. Test: open listener, accept many, exit without close(), all TCP slots reclaimed, later socket() no ENOBUFS |
| M-032 | M | recvfrom() consumes the datagram and accept() consumes the connection even if copying the peer address to user space fails. Both do the socket op first then hw_write_sockaddr(); the return of hw_write_sockaddr() is ignored, so an invalid addr_uptr loses the connection/datagram with no -EFAULT. Reachable by passing a bad pointer; no SMP race. Distinct from H-016/H-019/H-026 (here the caller ignores the copy failure and keeps the side effect) | fixed | kernel/arch/x86_64/linux_socket.c: hw_sys_accept() (void)hw_write_sockaddr ~204; hw_sys_recvfrom() ~417-419; hw_write_sockaddr returns -1 on bad ptr ~35-57. Fix: on copy failure propagate -EFAULT; for recvfrom prepare the datagram before removing it from the socket (or make semantics explicit); for accept handle the failure before consuming the child, or roll back the new FD/backlog |
| M-033 | M | open() truncates file size to 32-bit so fstat() returns a wrong st_size. vibeos_fs_lookup() returns 64-bit node.size but open() casts it to uint32_t into hw_fd_t::size (which is uint32_t); fstat() then passes f->size to hw_write_stat(), so a file >4GiB reads st_size mod 2^32. Reachable by open()+fstat on a fs declaring a >4GiB file. Distinct from M-018 (that fixed the by-name newfstatat path; here it is lost earlier, in the FD state) | fixed | kernel/arch/x86_64/arch_hw_internal.h: hw_fd_t.size uint32_t ~143-146; arch_hw.c hw_sys_open size=(uint32_t)node.size ~6220-6225, f->size=size ~6231-6233; hw_sys_fstat passes f->size ~8413-8417. Fix: make hw_fd_t::size (and pos) uint64_t and propagate the full size; widen lseek/pos too or read/lseek still truncate. Test: node >4GiB, open()->fstat() returns exact size |
| M-034 | M | kernel entry point not validated against loaded segments. The ELF parser copies e_entry straight into entry_point (PE: image_base+entry_rva); uefi_validate_segment_layout() checks segment size/range/overlap but NOT that entry_point falls inside a loaded, executable PT_LOAD/section. uefi_boot_handoff() then jumps there after ExitBootServices. A crafted/corrupt image with valid segments but e_entry outside them jumps to an arbitrary/unmapped address -> immediate crash instead of a clean reject. Reachable by replacing the kernel ELF on the ESP | fixed | boot/bootloader_core.c: ELF entry ~477, PE entry ~574-575; boot/uefi_pe_loader.c validate ~13-48 (no entry check), propagate ~75; boot/uefi_boot_handoff.c call ~142-148. Fix: after parsing, require entry_point canonical and fully inside a loadable EXEC segment (PE: checked image_base+entry_rva); reject before AllocatePages/ExitBootServices |
| M-035 | M | a KERNEL task does not preempt a running NORMAL one before its quantum ends: hw_higher_class_runnable() only tested mine == IDLE, while the policy orders KERNEL > NORMAL > IDLE and says a higher class always prevails | fixed (verified against the code: the loop compared only for an IDLE current task) | the rule moved into vibeos_sched_policy_should_preempt() in sched_policy.c - candidate class < current class, so a class added later needs no special case - and the arch function only builds the runnable mask and calls it. Host test in sched_policy_tests.c (KERNEL over NORMAL, NORMAL over IDLE, nothing over KERNEL, no preemption within a class). Found while verifying, and corrected in the same change: the reviewer says the adopted kernel task is a KERNEL-class task; it is not - hw_task_adopt_kernel never admits it to the policy, and no task anywhere is admitted as KERNEL, so today the fix has no observable effect in a boot (the defect is real in the code, latent in the system). My first version treated an unadmitted slot as "unknown, so preempt", which made the kernel task lose the core every tick and turned the boot gate red with quantum_not_spent - now it is NORMAL, as vibeos_sched_policy_class already says, with a test. The arch wiring itself is not host-testable (hw_higher_class_runnable is static in arch_hw.c); it is covered by the boot |
| M-036 | M | PROT_EXEC was not enforced: no user leaf ever carried NX, so every user page - stack, heap, a PROT_READ mapping - was executable and W^X was a property of the API only | fixed (verified, and wider than reported: the arch layer builds most user leaves itself - ELF segments, stack, brk, mmap - not only through vibeos_vmspace_leaf_flags) | NX on every user leaf unless executable is granted: leaf_flags, vibeos_vmspace_protect (both directions), ELF segments (X from p_flags), stack, brk, mmap and mprotect (PROT_EXEC now passed through). Three places rebuilt an entry and would have dropped the bit silently, each fixed and tested: the clone for fork, both copy-on-write installs, and swap-out/in (NX rides in the not-present entry). EFER.NXE is enabled on every core in hw_enable_syscall (the AP trampoline sets only LME), with a CPUID check. Reported by pageinfo (VIBEOS_PAGE_NX) and asserted from ring 3 in the ABI self-test: RW page has NX, +EXEC clears it, -EXEC sets it again. Host tests: leaf/protect/clone/COW/sole-owner in vmspace_tests.c. Not covered: an actual instruction fetch from a data page raising #PF |
| M-037 | M | st_blocks = (size + 511) / 512 wraps for a size within 511 of 2^64, giving 0 | fixed (verified) | vibeos_ceil_div_u64() in include/vibeos/ceildiv.h (n / u + (n % u != 0)), used by hw_write_stat. Host test at UINT64_MAX and UINT64_MAX-510, red with the old form. The other + 4095) / 4096 sums in arch_hw.c are boot-time sizes from the memory map, or lengths M-006 already bounds - not touched. Re-reported 2026-09-24 against arch_hw.c ~8400, where hw_write_stat was before the syscall handlers moved to kernel/abi/linux/fs.c; the fix and its test were in place. What was missing was the sabotage: core-ceildiv.txt puts (n + unit - 1) / unit back and test_ceil_div_no_wrap goes red |
| M-038 | H | pageinfo read *(uint64_t *)phys from a frame it did not own: a sibling's munmap frees it, another process is handed it, and first_word returns that process's bytes (cross-process disclosure) | fixed (verified; High rather than Medium - it is a disclosure reachable from ring 3 with no flag) | new vibeos_frame_try_get() (a reference only if the frame already has an owner - it will not resurrect a free one). pageinfo pins the frame, re-reads the PTE and proceeds only if it still names the same frame, and takes frame identity, owner count and first_word from that one pinned instant; if the page is gone it reports flags only. The output copy also moved to the fault-tolerant vibeos_uaccess_copy (the kernel wrote out_uptr directly). Host test for try_get, red if it resurrects a free frame. The race itself is not reproduced: the window is a few instructions between two cores |
| M-039 | M | ntfs_index_scan read $INDEX_ROOT's value_off/value_len straight from the attribute header and used them unchecked to build root and bound ntfs_index_walk - the H-011 defect (ntfs_data_attr, $DATA) never applied to the directory path. rec is a stack array in every caller (ntfs_op_lookup, ntfs_op_list), so a crafted directory record reads past it: a stack disclosure or crash reachable from an ordinary lookup or listing on a mounted volume, not a privileged path | fixed (verified; external report) | ntfs_index_scan in kernel/fs/ntfs.c, mirrors ntfs_data_attr's bound: attr_len recomputed from the attribute (find_attr's own check, not trusted secondhand), value_off required in [0x18, attr_len], value_len required <= attr_len - value_off. First proof was a genuinely out-of-bounds value_off (0xFFF0): AddressSanitizer aborted on stack-use-after-return in rd32, called from a frame that had already returned (ext2_map_block's) - real, but not deterministic (a 16-bit offset may or may not clear an 8 MiB host stack depending on call depth; the same input passed clean on a later run with the defect still in place). The gate is test_ntfs_index_root_wrap now: a second, well-formed index blob planted elsewhere in the same record (still in bounds) that a corrupted value_off points at instead of the real one - the unfixed driver lists the decoy's name, "HACKED", proving the cross-attribute read without needing UB to be caught in the act. cases/fs-ntfs-index-root.txt, red on that assertion. Green under a plain host build, gcc, clang, 3 boots and ASan |
| M-040 | M | hw_sys_write's small-file buffering loop dereferenced the user pointer directly (f->wbuf[f->wlen++] = (uint8_t)p[i2]) instead of the fault-safe vibeos_uaccess_copy every other buffered path in the same file uses - the dispatcher's row validates the range once, before the handler runs, but that check and this read are two instants (H-010's family): a sibling thread of the same process can munmap the buffer in between and the kernel takes the ring-0 fault instead of returning EFAULT. Reachable by any program with a second thread and no privilege beyond that; survived C4's move of the range check into the dispatcher, which changed where the range is validated but not that this read still trusted the validation to still hold | fixed (verified; external report) | hw_sys_write in kernel/abi/linux/fs.c: the loop copies through vibeos_uaccess_copy into the free space in f->wbuf, returning -EFAULT if the very first copy of the call faults (matching this file's convention elsewhere: total > 0 ? total : -VIBEOS_EFAULT, here 0 bytes are ever committed before the copy either way, so the two are the same). The race itself is not reproduced: it needs a second thread racing munmap against the syscall, which none of this project's existing test programs do; the recovery mechanism itself is proven by uaccess-recovery.txt, and the fix is the same pattern already applied and reproduced for H-010's other five sites |
| M-041 | M | swap-in never frees the slot it read from: vibeos_vmspace_swap_in (vmspace.c) brings a page's contents back and installs a present PTE, but the layer beneath it (swapmap.c) explicitly must not be depended on by vmspace.c (anon.c's own comment on the eviction side: it "is the layer below this and must not depend on it"), so nothing inside vmspace.c returns the slot - and nothing above it did either. hw_handle_cow_fault (arch_hw.c), the only real caller, discarded the slot number it read via vibeos_vmspace_swap_slot and never called vibeos_swap_free. Every page a process ever faulted back in permanently retired one swap slot; the eviction side (anon.c) already frees correctly on its own failure path, which is why only the bring-back side leaked | fixed (verified; external report) | hw_handle_cow_fault in kernel/arch/x86_64/arch_hw.c: the slot number vibeos_vmspace_swap_slot already returns is kept, and vibeos_swap_free(slot) is called once vibeos_vmspace_swap_in succeeds - the only place that knows both that the bring-back worked and which slot it emptied. Not independently reproduced: the leak needs sustained memory pressure to trigger repeated swap-out/swap-in over one boot, which no existing test drives, and the caller is architecture-only code with no host-test path; vibeos_swap_alloc/vibeos_swap_free's own round-trip is already covered by the boot-time swap self-test (io_bringup.c) and by vmspace_tests.c's host coverage of swap_out/swap_in themselves, neither of which exercised the missing call |
| M-042 | M | Code scanning #124-126 (cpp/overflowing-snprintf, reported high): the C6 diagnostics torture (tests/kernel/diag_torture.c) built its expected lines with n += snprintf(buf + n, cap - n, ...). snprintf returns the length it would have written, so after one truncation n exceeds cap and the next cap - n wraps to a huge size_t - a write past the buffer. Test code, and every expected line fit its buffer with room to spare, so it never fired; nothing prevented the next longer dump from overwriting the torture's own stack | fixed (verified; code scanning) | One bounded app() helper (vsnprintf, format(printf) attribute kept) replaces all six such calls, including three the scanner had not flagged; a model text that does not fit fails the torture instead of being cut, because a cut model compares the wrong string. core_c6_diag.md |
| M-043 | M | Code scanning #120-122 (cpp/stack-address-escape): vibeos_abi_linux_register keeps the caller's row pointer for the life of the registry, and the host ABI test handed it arrays on the stack, leaving the registry pointing into a dead frame until the next vibeos_abi_linux_reset. Nothing read it in that window; the first test to query the registry without resetting would have read whatever the stack held. The kernel's own caller (dispatch.c) passes static tables | fixed (verified; code scanning) | The test's tables are static; abi.h states the contract (rows must outlive the registry). Not reproduced as a failure - there was no reader in the window - which is why the fix is the contract, written where the next caller reads it |
| M-044 | L | Code scanning #123 (cpp/declaration-hides-variable, note): in ntfs_index_scan an inner attr_len ($INDEX_ALLOCATION's length) shadowed the outer one ($INDEX_ROOT's, added by M-039). Correct today; in a parser of an external format, two attributes' lengths under one name is how one bound gets checked against the other | fixed (code scanning) | Inner variable renamed alloc_len, with the reason beside it |
| M-045 | L | Code scanning #118 (cpp/loop-variable-changed): tcp_abort_socket (kernel/net/inet.c) shifted a listener's backlog with the search loop's own counter, correct only because a break followed. Sabotaging the removal (leave the child queued; do not close the gap) went NOT RED against the host suite, and the reason is structural: a child joins a backlog only on becoming ESTABLISHED, and the routine is only called for SYN_RECEIVED children (H-030) and orphans (M-023), neither queued - the loop is unreachable today | fixed (code scanning); unreachable code recorded | Search and shift use separate counters. The unreachability is written beside the loop, with the two sabotages that would become runnable once a caller aborts a queued child; no case file, because an empty one would make check-subsystem.py count net/inet as sabotage-covered |
| M-046 | L | Code scanning #111, #112, #117, #119: an unused local and a comparison made dead by the line above it in sched_torture.c; irq_handler in kernel_tests.c, orphaned when kernel/core/interrupts.c was removed; an inner k in hw_sys_execve (kernel/abi/linux/proc.c) shadowing the function's own k | fixed (code scanning) | Removed, removed, removed, and the inner counter renamed ci (17 lines, every one only the rename) |
| M-047 | H | NTFS reads had no volume bound at all: exFAT and ISO9660 were bounded by their partition in H-029, and vibeos_ntfs_t did not even have a field for it, so a run list pointing past the partition read the neighbouring partition's sectors through an ordinary open() and read() (the block cache bounds by the whole disk) | fixed (verified; external report) | ntfs_read_sectors, the one funnel every NTFS read goes through, is held to the tighter of two limits: the partition table's (part_sectors, set by storage.c as for exFAT/ISO) and the size the volume declares for itself (boot sector 0x28, plus the backup boot sector), which also bounds a volume mounted with no table. Checked as count > limit - sector, never as a sum. Red first: test_ntfs_read_bounded_by_volume (cluster 100 refused, cluster 50 still served, for each limit). fs-ntfs-bounds.txt cases 1-2 |
| M-048 | H | NTFS directory index walker: every bound was off + n > len with off read from the index header - a first-entry offset within 16 of 2^32 wraps the sum, passes the check, and the entry is read from four gigabytes past the node. The report placed it in the $INDEX_ALLOCATION branch as M-039's twin; verified, the defect is in ntfs_index_walk, which serves both the resident root and every INDX block - M-039 bounded the root's value, not the offsets inside it | fixed (verified; external report, location refined) | Every bound rewritten as n > len - off after off <= len; an impossible first offset is a malformed node (counted, ntfs_bad_metadata), not an empty one. test_ntfs_index_offset_wrap: red by crashing the suite - the only offsets that wrap point 4 GiB away, so there is no in-bounds discriminator on a 64-bit host (the M-039 lesson, applied: said rather than dressed up as an assertion). fs-ntfs-bounds.txt cases 3-4 |
| M-049 | H | ext2 readdir (ext2_op_list) copied name_len bytes with no bound at all - lookup checked the block - so a name_len larger than its record copied the next records and, at the end of a block, up to 254 bytes of the kernel stack array past it into an unprivileged readdir() | fixed (verified; external report) | Both paths bound the name by its own record (name_len <= rec_len - 8), which also stops lookup's comparison running into the next record. Red first with an in-bounds discriminator: an 8-character name in a 16-byte record with name_len 12 made the unfixed driver return the next record's inode bytes as a ninth character, deterministically. fs-ext2-names.txt |
| M-050 | H | net.c socket address helpers (bind/connect/sendto read; accept/recvfrom write) dereferenced the raw user pointer. The dispatcher's range check and the access are two instants - in accept/recvfrom separated by a blocking wait - so a sibling's munmap made the kernel fault in ring 0 and panic (H-010's family). Also found, not in the report: netctl ops 0 and 3 wrote the user struct directly while holding g_net_lock | fixed (verified; external report + sweep) | Both helpers copy through vibeos_uaccess_copy via a local; accept's undo path, written for a failure that could not happen, now can run. netctl gathers under the lock and copies out after releasing it. The race is not reproduced (it needs a thread racing munmap against a blocking accept); the mechanism is gated instead - see M-052 |
| M-051 | H | read() on a regular file handed the user's buffer straight to the filesystem, which copies into it with plain memcpy - the same race-to-panic | fixed (verified; external report) | Read through a one-page kernel buffer (512 bytes on the stack if no page is free) and copied out with vibeos_uaccess_copy. A page rather than a small chunk because the FAT reader walks the cluster chain from the start on every call - small chunks would make a large read quadratic - and a kernel stack is 8 KiB or less. Not reproduced; gated by M-052's check |
| M-052 | H | write() to stdout/stderr read the user's buffer directly - M-040 had fixed only the file branch - and did so under the console lock; the NUL detector in the same branch also read it directly. The report's deadlock half does not hold: the console lock is recursive per core, so the panic path on the faulting core re-takes it; the panic is real. The sweep found four more of the same shape: getdents64, uname, prlimit (and M-050's netctl). Eight sites in one review | fixed (verified; external report + sweep) | The console branch copies in 128-byte chunks inside the one critical section - safe, because a faulting copy resumes at its recovery point before the trap handler prints anything; the detector reads through copies too; getdents64/uname/prlimit build in the kernel and copy out once. New check, check-user-access.py, in check.sh: in kernel/abi/linux, every integer-to-pointer cast must be an argument of vibeos_uaccess_copy, hw_copy_user_string or the pipe module (which copies through that callback); two reasoned exceptions (pageinfo reads kernel frames). It reports 11 violations at the parent commit and 0 now; core-user-access.txt puts three of the shapes back and it goes red each time |
| M-053 | M | GUI canary written with 8-byte stores at back + width*height*4: on a screen with an odd pixel count that is a 4-byte boundary. Found by CI (clang Debug, UBSan: "store to misaligned address") on the first push of C7 step 4; gcc's UBSan in the nightly passed the same code | fixed (verified) | The canary starts at the pixel count rounded up to even (VIBEOS_GUI_GUARD_PIXEL), the buffer size is one macro (VIBEOS_GUI_BACK_BYTES), and init refuses a buffer that is not 8-byte aligned, by name. The host test's screen is 201x151 so the odd case runs on every build. io-gui-ubsan.txt puts the old placement back and the CI sanitizer configuration (verify-host-sanitized.sh) reports the same error |
| M-054 | L | check-exec-layering.sh exited 0 when the function it guards was gone - the one failure path left after the others were fixed to exit 1, so an exit-code reader (sabotage.py's verifier) saw a pass | fixed (verified; external report) | exit 1. core-exec-layering.txt case 2 renames the function: NOT RED against the old script, red against the fixed one |
| M-055 | L | .f.sh printed and asserted a list of checks that left out task-identity and exec-layering - and user-access, net-stable and fuzz-rc, which came later | fixed (external report) | All asserted; the comment above the loop already said "every check it prints, it asserts" |
| M-056 | H | vibeos_vmspace_swap_out had no final re-check (external report, one line). Reading the code found it wider: reclaim runs from the allocation path on any core and evicts from another process holding none of its locks, so (1) the owner exiting freed the page tables being walked and CASed into, (2) munmap freed a slot still being written, (3) a fault read a slot not yet written - the entry said "swapped" before the write. The anonymous tier's hand and counters were unlocked too | fixed (host-verified; external report) | Claims in the rmap keep the victim's page tables alive (teardown waits in forget_root, bounded by a named panic); the page stays present and read-only-marked (VIBEOS_PTE_SWAPOUT) while written, a store cancels, and one final exchange commits - fork, munmap, mprotect and compaction all make it fail. Honoured by the fault handler, hw_user_range_ok and every entry rebuild. First host test of anon.c; 11 new sabotage cases all red by name. No run on a machine has ever reclaimed an anonymous page - the soak included (anon_scanned=0 at 12,000 rounds) - so the machine half is proved by review and host tests only. See mm_swapout_protocol.md |
| M-057 | H | vibeos_cache_get handed out a page-cache frame with no reference and the cache lock already released; the loader mapped it and the exec reader copied from it. An eviction on another core in between frees the frame, and the mapping then takes its reference on a page already handed to somebody else - whose exit later frees it | fixed (internal) | vibeos_cache_get_ref takes a frame reference under the cache lock; all three users give it back after copying or after the mapping holds its own. Host test: a referenced page survives an eviction and goes on the last put; mm-cache.txt removes the reference and it goes red |
| M-058 | M | The clean reclaim tier never reported a frame freed. It told an eviction from an empty slot by looking at the entry - and cache_evict empties an entry it evicts, so a page just given back looked like a free slot: the loop stopped at its first successful eviction and counted it as nothing (freed_clean=0 in every log there is) | fixed (internal) | cache_evict says whether it freed a frame; empty slots are skipped with a bound. Found by the first test to ask reclaim for a page; mm-cache.txt puts the old judgement back and it goes red |
| M-059 | L | The free-page poison watch printed under the frame layer's lock, taking the console lock inside it | fixed (internal) | Records under the lock; the allocating core prints after leaving the layer. Arch code with no host test - reasoned from CLAUDE.md's earlier deadlock of the same shape, not demonstrated |
| M-060 | H | Under svc-press's allocation load (28 MiB, far above the watermarks) the machine stops: cores fault in ring 0 at garbage addresses while inside the timer interrupt on its IST. Cause: the loader handed over the memory map it took before loading the kernel, so the kernel image was in the frame pool, and the allocator reached the page holding the IDT and the timer's IST | fixed (internal) | The loader refreshes the map after its last allocation (vibeos_bootloader_refresh_memory_map); the kernel reserves its own image regardless and prints in_free_map=, which the gate asserts is 0. Mechanism shown both ways with svc-press: both halves removed wedges at 112 blocks, the old signature; the reservation alone survives to 256 with the gate red on kernel_image_in_free_map. Cases boot-memory-map.txt (boot) and boot-memory-map-core.txt (host). Write-up in mm_reclaim.md |
| M-061 | M | A munmap burst overflows the TLB quarantine's 512 slots, and an overflowing frame is leaked by design (H-015) - svc-press unmapping 64 MiB loses 2,486 frames, ~10 MiB, for the rest of the boot | fixed (internal) | A frame is parked only when another core has the address space loaded (loading_cr3/loaded_cr3, published around every CR3 write); single-threaded unmaps never park. svc-press: overflow 0, 8 frames lost. svc-press now runs every boot and is gated; the old munmap_never_deferred_a_frame became timing-dependent (2 boots in 10) and was replaced by a kernel self-test of both cases. mm_tlb_quarantine.md, cases mm-tlbq-holders.txt, svc-press.txt, services-press.txt |
| M-062 | L | The free-while-mapped watch walks page tables after the frame layer's lock is dropped, then reads the owner count: another core can take the frame, map it, unmap it and free it again in between, which reads as a frame freed while still mapped. svc-press exiting with 64 MiB beside svc-stress produced four such reports in one boot of three, every one owners_now=1 or 2 | fixed (internal) | The frame layer counts hand-outs per frame (the unused lru_prev became handouts) and tells the watch the count at release; a frame handed out since is free_watch_torn, not a defect. Same reproduction: 7 torn, 0 reported. A real one (fork's reference given back) is still reported, with owners_now=0. Cases mm-frame-handouts.txt (host), mm-free-watch.txt (boot) |
| M-063 | H | A swapped-out entry was invisible to every operation but the page fault: fork skipped it (the child had no page there), munmap and teardown skipped it (the slot leaked, and after a munmap the entry still named a slot swap would give to somebody else), mprotect refused it, and the syscall range check returned EFAULT for a buffer in swap. Page-in restored every page user-writable - a read-only mapping or a PROT_NONE guard came back as ordinary memory - and took a second reference, so each page brought back outlived its process (764 frames lost in a boot that brought 756 back) | fixed (internal) | Swap-out keeps write/user/COW in bits the CPU ignores while the entry is not present, and page-in restores them; page-in adopts the caller's reference; munmap and teardown release the slot through a new swap_release hook, fork brings the page back through swap_bring_in (retrying if reclaim takes it again), mprotect updates the saved bits, the range check accepts a swapped entry. test_swapped_entries_are_mappings + mm-swapped-entries.txt (7 cases; the one that went NOT RED found a hole in the test, now closed). Gate: swap_slots_leaked, fork_of_swapped_page_failed. A second race found under load: the bring-in freed the slot it had read before the page-in, and a sibling thread could bring the page in and reclaim send it out again in between - the old slot freed twice, the new one leaked (swap_double_free=6 in one run). The page-in now releases the slot it consumed itself, through swap_release; nobody outside frees one |
| M-064 | H | Swap had never run on a real medium. The gate's boot disk is QEMU's vvfat - a host directory presented as FAT - and a write into the middle of an existing file does not read back: every slot past the second returned slot 0's or slot 1's contents. The one-slot boot round trip passed every boot | fixed (internal) | The gate boots a real FAT image built by make_esp_image.py (VIBEOS_SMOKE_ESP=image, now the default; vvfat still selectable). The kernel writes eight slots before reading any back and refuses an area that aliases, and every page-in is checked against a hash of what was written (SWAP_CHECK MUSTBEZERO read_mismatch). Cases mm-swap-medium.txt, mm-swap-check.txt |
| M-065 | H | TLB shootdown: two defects the first reclaim load made ordinary. It chose targets by the current task's cr3, so a core tearing down an address space from the kernel's tables - holding nothing of it, waiting for a reclaim claim with interrupts off - was targeted by the swap-out holding that claim, and the machine stopped. And one machine-wide pending count was reset by every shootdown, so two at once wiped each other's count and one could return before its targets flushed | fixed (internal) | Targets are the cores whose CR3 holds the space (loading_cr3/loaded_cr3, M-061); each target is waited on through its own cr3_generation; a core spinning with interrupts off answers flush requests addressed to it (hw_tlb_service_flush, in spinlocks, the claim wait and the shootdown wait). The gate compares flushes seen with targets asked (tlb_flushed/tlb_targets): acknowledgements stopped meaning it when two requests became one flush. The deadlock and the wiped count are races with no deterministic case |
| M-066 | L | virtio-net polls, but left INTx enabled as UEFI had it - on line 11, which virtio-blk routes and handles. A received frame raised a level-triggered interrupt that only the next transmit cleared, into a handler that acknowledges a different device | fixed (internal) | INTx Disable set at probe; the line is printed. The third driver to meet bit 10, after AHCI and virtio-blk. Not shown to be the cause of M-067 |
| M-067 | M | With the real boot image, boots wedged in the AHCI driver - the log disk. The decisive one: all four cores spinning in ahci_lock under vibeos_blk_write, interrupt counts not moving (no storm), and nobody running the code that held it. The lock was a bare test-and-set with interrupts left on, and its wait halted with them on, so the holder could be interrupted or taken off its core in the middle of a command; every core that logged then queued behind a holder that could not get a core back | fixed (internal) | The driver takes the registry's lock (vibeos_dev_lock), which masks interrupts while held, names its holder in a deadlock report and refuses a core that already holds it. Waits poll; the completion interrupt is still raised and counted after the unlock. Seen about one boot in twelve before, and in two of the first four boots on the image; none in 24 after. A race with no deterministic case. virtio-blk keeps its own lock with interrupts on - its waits run in syscall paths with interrupts already off, so they never halt there - and is the same shape if that ever changes |
| M-068 | M | The kernel announced "all user tasks retired" and sampled the frame accounting while svc-reclaim was still running - thrashing at the minimum with swap full, in a boot where the first version of the load overran what reclaim could return. The wait loop counts READY, RUNNING and BLOCKED as alive and the task's user flag is stable, so it must have been seen in some other state for an instant; not found by reading | open (internal) | The gate's premise check now fails any boot in which a user task exits after that announcement (it had only checked the stress service's line), so the next occurrence is a named failure rather than a leak-shaped number. Evidence: .boot-evidence/recl-run1/sab-3.log, announcement at line 545, the task's exit at 706. Instrumented: the announcement is followed by [SCHED] RETIRED_SLOTS, the state of every user slot that is not free. Not seen in 48 boots since 2026-09-27: the boot it appeared in was the one where the load's first version ran as an init service and init, whose oneshot had finished, exited while it was still running - an orphan, an arrangement that no longer occurs since the load moved into the self-test script. Not seen in 24 thrash boots (clock second chance disabled) nor in 16 boots of the arranged teardown race; all 40 printed a RETIRED_SLOTS line with zombies only 2026-10-09: dormant. The nightly's Boot Smoke x5 ran 145 boots from 2026-09-11 to 2026-10-09 with one failure, net_sock_fd_aba (fixed in 4a216f1), and every CI push boots four more; the gate fails on this signature and it never appeared. Nothing found by reading in this pass either. Kept open, not closed: absence in boots is not a mechanism, and the instrumentation stays so the next occurrence arrives described |
| M-069 | M | The anonymous tier has no notion of recency: its clock takes any single-owner page, the one about to be touched as readily as one nobody will touch again. Under svc-reclaim, swap is full within one 256 KiB block of the first crossing of the low watermark (past_low=1, swap free 0), and the pages taken are faulted straight back | fixed (internal) | The clock reads the accessed bit now (vibeos_vmspace_clear_young): a page used since the hand last passed is spared once and its mark cleared - the second chance. On the reclaim load: the full 48 blocks past the low mark instead of 1, swap half used instead of full, about half the evictions, 91k pages spared (anon_young). test_anon_reclaim_spares_the_young, mm-anon-young.txt. Before: the accessed bit is what a clock is for; anon.c did not read it. svc-reclaim stays inside what reclaim can return (stops at three quarters of swap) and releases each block as it checks it, so the boot is not starved - but the policy is the thing to fix, and svc-reclaim's past_low and swap figures are how to see it improve |
| M-070 | M | One boot in 24 with the reclaim load: svc-reclaim - single-threaded, no fork - killed by a not-present read in a block it had not yet checked, with swap_double_free=1 and one slot still allocated at the end. The page's entry was neither present nor swapped, so the page was lost and a slot freed twice; the swap counters otherwise add up (1296 out = 1278 in + 18 dropped), swap_out_raced=0 | open (internal) | Not explained by reading. Evidence .boot-evidence/recl-run4/repeat-fail-10-*. Next: make the swap map's double free say which slot and which caller, and the ring-3 kill say what the leaf entry held - both counts alone say nothing. Instrumented: a second release of a slot prints SWAP_DOUBLE_RELEASE with the slot, the operation doing it and the one that did it before; a ring-3 page-fault kill prints the leaf entry. Not seen in 48 boots since M-069 stopped the load thrashing - which narrows it without explaining it 2026-09-27: its boot had svc-press exit beside the load - a large teardown while reclaim ran, the arrangement M-073 turned out to be. That arrangement is now made on purpose (svc-reclaim's abandon phase; rmap_claim_waits went from 0 on every boot to 1 in 9 of 16) and 24 thrash boots were run as well: no SWAP_DOUBLE_RELEASE, no lost page, in 40. Seen again 2026-10-01, and for the first time with the instrumentation (docs/abi/ L1 step 8, 1 boot in 18; evidence .boot-evidence/l1-step8/repeat-fail-5-*): during the reclaim load, with swap 76% full, init - not svc-reclaim - was killed by a not-present read at 0x8000001094 whose leaf entry was 0x8000000000158404: a swapped-out entry naming slot 0x158. Then SWAP_DOUBLE_RELEASE slot=0x158 now_by=destroy before_by=swap-in pid=5. So this time the entry was a swapped one, and its slot had already been given back by a swap-in running as pid 5, a different process: two address spaces believed they owned slot 0x158, or one slot was handed out twice. The swap map is a bitmap with no count, so a slot has exactly one owner by construction, and fork brings a swapped page back before sharing it - which leaves a slot allocated twice, or an entry left behind by a swap-in that raced, as the things to instrument next: record on each allocation who took the slot, as each release already records who gave it back. The step that saw it raised the user stack from 4 pages to 64, so every process has sixty more anonymous pages for reclaim to take, most of them shared with a forked child; that may be why init was the victim. 2026-10-01, docs/abi/ L3 step 1: a second and a third capture, a race found beside it, and no explanation yet. Second (.boot-evidence/l3-step1/m070-second-*): slot=0x158 now_by=unmap before_by=unmap pid=0xb, and svc-reclaim killed on a swapped entry naming slot 0x14b, released again at its teardown. Third (m070-third-*): init again, the same address 0x8000001094, entry 0x800000000017a404, now_by=destroy before_by=swap-in pid=5. A correction to the reading above: pid= is the process running when the second release happened, not the first - so "a page-in in another process" was never established, and the conclusion drawn from it (two address spaces naming one slot) is a guess. What the three do establish: the reclaim load is running; the victim is killed on a valid swapped entry (0x404: swapped, user, read-only) for a page it owns, so its page-in failed; and by the time the address space is torn down the slot is already free, last given back by an operation named swap-in. A race was found while reading for this and fixed - fork mapped a frame read out of a read-only entry after a page-out had released it (clone_one now pins with vibeos_frame_try_get and re-reads the entry; compact_tests.c reproduces it through the fork hook; fork_frame_gone, fork_entry_moved) - but the third capture came with that fix in and both counters at zero, so it is not this. Rate: 3 in about 60 boots since L1 step 8 raised the user stack to 64 pages, against 0 in about 90 before; sixty cold pages per process is sixty more for reclaim to take, most of them read-only copy-on-write after a fork. Instrumented further: `SWAP_IN_FAILED why=no_frame |
| M-071 | M | mprotect held the address-space lock for the region list only and narrowed the page tables after releasing it, so a fork on a sibling thread could copy an entry mprotect was halfway through changing - left that way because a sibling spinning in the lock could not answer the shootdown IPI, which stopped being true with M-065. Reading it found the other half: the range check and the pass asked for a present entry, so mprotect on a page in swap was refused with EFAULT | fixed (internal, 2026-09-27) | The lock is held from the range check to the last entry; a swapped entry is mapped and has its saved permissions changed. svc-reclaim makes every block read-only before reading it back; mm-mprotect.txt (check back to present-only) goes red with reclaim_load_failed. The lock itself has no case - recorded as untested. core_c5_residue.md |
| M-072 | M | Descriptors were copied per thread: a descriptor one thread opened did not exist in the others, and a close in one left the rest holding it. And each thread's exit released every socket of its process by thread-group id, so a worker that finished closed its process's connections | fixed (internal, 2026-09-27) | The table is in hw_procstate_t; fork and exec copy it, a thread shares it. Claims, close and dup2 under a per-process lock (open claims before the disk lookup); the last thread to leave closes the table and releases the sockets. THREADS_C5_FILES went red against the previous kernel and is gated; fd-shared.txt has two cases; a leave that would take the count below zero is files_double_leave, must-be-zero - the first version of the clone case went NOT RED without it. core_c5_residue.md |
| M-073 | L | rmap_missing_remove fired about once in fifty boots. An eviction that claimed a page before its process's teardown commits after forget_root has removed every holder of the root, and its remove finds nothing - correctly: the frame is released once and the teardown frees the slot | fixed (internal, 2026-09-27) | Named by the instrumentation on the first reoccurrence (caller vibeos_vmspace_swap_out, an address in svc-reclaim's blocks, in a boot where svc-reclaim exited early with its blocks mapped). forget_root marks the claims it waits on and such a remove is counted as removed_after_forget; test_eviction_during_teardown, and a sabotage in mm-rmap-claims.txt. core_c5_residue.md |
| M-074 | L | The mount table (kernel/fs/vfs.c) had no lock. Attach appends and detach moves the last entry into the hole, so a resolve beside either can read a half-moved entry and return the wrong mount for a path. Latent: every caller today runs during single-core bring-up - serialised by accident, the page cache's shape in CLAUDE.md. Flagged by check-subsystem's state_without_lock | fixed (internal, 2026-09-28) | The table takes its own lock, registered by the architecture before the first attach (vibeos_fs_set_lock, as pipes and frames do). A call with none registered is the must-be-zero mount_unlocked, so a lock configured by nobody fails the boot. test_mounttab checks every operation takes and releases it once; fs-mounttab-lock.txt (two cases, host) and fs-mounttab-lock-arch.txt (registration removed: mbz_mount_unlocked) go red |
| M-075 | L | The fork guard's statistics were plain ++, correct only because its one caller holds g_sched_lock - a second caller outside it would lose counts. The verdict itself has no state: limits written once at init, counts passed in by the caller. Flagged by check-subsystem's state_without_lock | fixed (internal, 2026-09-28) | Atomic adds, and a comment saying why the guard takes no lock. No case: a lost increment needs two cores inside the guard at once, which neither a host test nor a boot arranges - recorded as untested |
| M-076 | M | The boot gate asserted the loop-mounted filesystem images by name for ext2 and iso9660 only. NTFS and exFAT were mounted every boot and asserted by nobody, while io-filesystems.txt recorded three red results for them - both rows removed, and the NTFS index allocation never walked - that the gate could not produce. And CI installed neither ntfs-3g nor exfatprogs, so there the images did not exist at all and would have passed as "no image" | fixed (internal, 2026-09-28) | The gate lists all four. exFAT passes on "OK (no marker)": no tool can stage a file into its image without mounting it, so a mount is all it proves and the verdict says so. The boot workflows install ntfs-3g and exfatprogs. The three prose cases are machine cases now (io-filesystems-gate.txt, io-filesystems-ntfs.txt) and were run red |
| M-077 | M | scripts/dev/trace-linux-binary.sh, which scripts/dev/README.md calls the method behind every syscall in the kernel's Linux layer, reported "(nothing - this binary's syscall set is covered)" for every binary. It looked for the handlers as case LSYS_ in arch_hw.c, which C4 replaced with table rows, and its name extraction had stopped matching strace's output - so both lists were empty and the comparison always succeeded | fixed (internal, 2026-09-29) | Found starting phase A0 of docs/abi/. It now extracts names the way corpus-measure.sh does, reads each syscall's state and phase from docs/abi/syscalls.md, and exits non-zero if strace's output yields no names at all - so an empty measurement is reported as a broken tool rather than as coverage. Checked on musl_hello (missing=0) and busybox cp -a (missing: chmod, chown, utimensat) |
| M-078 | M | fork cloned the parent's regions and page tables under the address-space lock, released it, and only then copied brk_cur and mmap_cur. A sibling thread's brk or mmap fits in that gap, so the child could hold the regions of one moment and the break of the next: its own next brk then maps over pages it already has, or gives back ones it never got. The cursor's load was atomic, which made each read safe and the pair no more one snapshot than before | fixed (team review, 2026-10-01) | Both are copied before the release (kernel/abi/linux/proc.c). Host test t_fork_snapshot_and_groups: the fake kernel fires a hook as the lock is released (kf_on_mm_unlock) that moves the parent's break and cursor, and the child must have the earlier values. Cases abi-fork-snapshot.txt 1-2, red. Not tested on four cores: the window is a few instructions |
| M-079 | M | setpgid(pid, pgid) accepted any pid of the session as pgid, without asking whether that process leads a group. A process could be put in a group numbered after a thread, or after a process that is itself in another group - a group whose number names nobody in it, for getpgrp to report and kill(-pgid) to aim at | fixed (team review, 2026-10-01; reported as a second M-078, renumbered) | A group is joined only if the process the number names has pgid == pid; making the target its own leader is unchanged. EPERM, as Linux answers when no such group is in the session. Same host test; case abi-fork-snapshot.txt 3, red. Still true: a group whose leader has exited cannot be joined, because the lookup is by the leader's pid |
| M-080 | M | A call on a descriptor - fchmod, fchown, futimens, fstatfs, every AT_EMPTY_PATH - walks the path its description remembers (linux_walk_fd). Open /a, rename it to /b, create a new /a: the call then changed or reported the new /a, the wrong object, through a descriptor that still looked valid. No race is needed. The gap was written down in L1 step 5 as "a description does not follow its file across a rename"; what was not said is that it can land on somebody else's file | fixed 4c703c5 (team review, 2026-10-01) | The walk's answer is compared with the node the description was opened on, and another node is ESTALE - a refusal, where Linux would act on the renamed file. Only as good as the filesystem's node numbers: tmpfs's carry a generation; FAT's are the directory slot, so a new file in the old one's slot is not told apart. Case abi-fd-identity.txt, red. To close: setattr and statfs by node instead of by path in the filesystem operations, so a descriptor's calls never walk Closed 2026-10-09: the filesystems answer getattr and setattr_node by node (tmpfs by inode and generation, FAT by directory slot), and fchmod, fchown, futimens, AT_EMPTY_PATH and fstat act on the descriptor's own file wherever it was renamed to, as Linux does; fstatfs asks only the description's mount. A filesystem without the question still walks and refuses with ESTALE. A file whose last name is gone is ENOENT where Linux would still act on it. FAT's node is its slot, so its limit is unchanged. Cases abi-fd-identity.txt (2), abi-fd-identity-stat.txt, fs-fat-node.txt (2), all red |
| M-081 | M | The page cache names a file by a small number, and the table of those numbers had thirty-two entries and never gave one back. A file with no number is read whole into the exec staging buffer - four megabytes when that fallback was written, 64 KiB now - so the thirty-third distinct program of a boot did not start if it was larger than that, and execve said not-found about a file that was on the disk. A boot runs about twenty; the twelfth LTP test of a run was the thirty-third | fixed b1fc301 (internal, 2026-10-02, found by LTP) | 512 entries, and running out is logged (no file identity left for the page cache). Not closed: an identity is still never returned, because a live process's regions name theirs (backing_id) and nothing counts who does. To close: count the regions and images that name an identity, and reuse one nobody names Closed 2026-10-09: the least recently looked-up identity is reused once the table is full, never one looked up in the last sixteen (a load reads at most two before mapping them). Nothing reads pages through a region's backing_id - an image is mapped at load, holding its own frames - so a stale id in a region only affects merging. Reading the table found two more defects: it had no lock (Linux exec and boot spawns call it from different cores), and it was keyed by path with the node seen first, so a file replaced at its path was served from the old one's pages; the node is compared on every lookup now. Shown with a table of 8: the boot passes reusing 19; with reuse sabotaged it fails, 24 files uncached and SQLite, Lua and glibc not starting. Counted in [EXEC] file_ids_reused=, file_ids_replaced= |
| M-082 | M | linux_stat_get and linux_walk_at_empty read the first byte of a path - to learn whether it is empty - with vibeos_uaccess_copy and no range check. A path is not a pointer a row declares, so nothing had judged it, and that copy is fault-tolerant, not permission-checked: ring 0 reads any page that is mapped. statx, newfstatat and every AT_EMPTY_PATH call therefore read one byte of memory the program may not - its own PROT_NONE pages, or the kernel's - and answered ENOENT if it was zero and EFAULT (from the checked walk that follows) if not: one bit a call | fixed (internal, 2026-10-02, found by LTP's statx03) | linux_user_ok before both reads. Host test t_ltp_l1; cases abi-ltp-l1.txt 1-2, red - after the fake kernel's own copy stopped refusing what ring 0 does not (it had hidden this from every host test). The boot's ABI self-test asks statx and newfstatat about a path in a PROT_NONE page. check-user-access.py did not see it; the other copies from user pointers in kernel/abi/ were read through and are covered by their rows |
| M-083 | M | A process group is the process's, and pgid lives in each task: setpgid wrote the one task the pid named, so the threads of a process could be in different groups for getpgid and for a signal sent to a group. And joining a group asked whether a task had that number as its pid - which a thread's id satisfies and a group whose leader has exited does not | fixed (team review, 2026-10-02; reported as a third M-078) | setpgid writes every task of the thread group, as setsid already did, and a group exists while any process of the session is in it (this replaces M-079's test of the leader). Host test t_fork_snapshot_and_groups; cases abi-fork-snapshot.txt 3-5, red. The number is still stored per task rather than in the process state - moving it is the cleaner shape and touches every reader |
| M-084 | H | The scheduler's policy (kernel/sched/sched_policy.c: classes, nice-weighted fairness, affinity) was never initialised on the machine. The architecture admitted every task into it and asked it to pick on every tick; with no slots every admission was refused, the refusals were ignored, the picker returned nothing and the machine fell back to round robin - and the policy was never charged the virtual time it picks by, so initialising it alone would have picked the lowest slot for ever. Class preemption (M-035), idle slices and nice did nothing on the machine for a month, while host tests and the torture were green | fixed (found in docs/abi/ L2 step 4, 2026-10-03, because setpriority changed nothing). Initialised beside the run queue with a panic if it refuses, charged on every core's tick; a refused admission is sched_admit_refused, must be zero, and the gate asserts policy_charged moved (sched-policy-live.txt, both cases red) |
H-015..H-020 and M-017 arrived 2026-09-14, one at a time, all verified against
the code. Four of them (H-016, H-018-read, H-019, H-020) are the "dozens of
validate-then-use sites" uaccess_recovery_open.md already flagged, now being
enumerated: one fault-safe-copy pass fixes them. H-017/H-018-rip are one theme
(a ring-3-controlled RIP reaching iretq); M-017 and H-015 stand alone.
Checked by the reviewer and not promoted, recorded so nobody re-reports them:
the futex waiter slot is kept until its own waiter returns (the earlier ABA is
closed); ARCH_SET_GS is refused because %gs holds per-CPU kernel state; and
hw_sys_lseek truncates the offset to 32 bits (the finding's own second item,
withheld by its author for want of a security chain - it stays here, unpromoted,
with the other validate-then-use sites).
Ninth review (2026-10-07, of HEAD 9fb7555, the A3-L4 week)
Static reading in four areas: four high findings and thirteen medium.
| ID | Sev | Finding | Status | Evidence / next step |
|---|---|---|---|---|
| H-031 | H | epoll_wait/pwait/pwait2: the row's OUT_VEC read maxevents at 64 bits and skipped the check over the cap, the handler read the low 32 - 0x1_0000_0001 wrote an event, with the program's own word, through an unjudged pointer into kernel memory | fixed fbd776b | a row says its count is an int (OUT_VEC32, VIBEOS_PTR_LEN32) and the engine reads it as the handler does. t_epoll writes through a static standing for kernel memory; abi-ptr-len32.txt 2 cases, red. poll and the vector calls compare all 64 bits, as the engine does |
| H-032 | H | mincore stored its vector under the mm lock; a store into a stack page not yet grown faulted into hw_stack_grow, which found the lock busy (held by the same task) and retried for ever, interrupts off | fixed bd2590e | chunk under the lock, store after. mm_busy records the holder's slot, and a stack fault under its own task's lock panics with the reason. The fake counts user copies that could fault made under the lock; must be zero over the whole Linux suite. abi-mm-uaccess.txt, red |
| H-033 | H | mprotect made a MAP_SHARED mapping of an O_RDONLY descriptor writable - on tmpfs and memfd, the file's own pages | fixed 4e6e699 | VIBEOS_PROT_NOWRITE on the region, kept across every change of protection, EACCES from mprotect. mm-nowrite.txt 3 cases, red |
| H-034 | H | nested epoll: ep_ready's 2.5 KiB snapshot recursed EP_DEPTH deep on an 8 KiB kernel stack | fixed 7ace0b0 | four entries at a time, a static assertion bounds the recursion's snapshots to 1 KiB (removing it stops the build, checked by hand). The host cannot see a stack overflow; abi-epoll-stack.txt proves the small snapshot is walked |
| M-085 | M | ks_fpu_save hands 512 bytes to the signal frame with the reserved and software bytes (416-511) never written: stale kernel stack to user | fixed 806e9a9 | the area is zeroed before FXSAVE; SIGNAL.ELF checks bytes 416-463 of a frame are zero. fpu-frame-clear.txt, boot, red (signal_l2_checks_failed) |
| M-086 | M | FAT writes are not bounded by the partition (fat_scan_mount: (void)sectors), so a crafted BPB writes outside it | fixed 63d4292 | the mount keeps the partition's length and every sector the driver reads or writes is judged against it (fat_sector_ok, counted as fat_out_of_volume). fs-fat-review9.txt, red |
| M-087 | M | fat_write_at grows the chain in memory and returns ENOSPC without saving the entry: clusters lost | fixed 4996643 | a failed write gives back the clusters it added (fat_chain_undo); fs-fat.txt re-anchored, red |
| M-088 | M | (agent) FAT zeroes a gap left by truncate or a far pwrite synchronously, under the driver's global lock | verified, kept by decision | true, and bounded by the gap's size. Releasing the lock between chunks lets a truncate run between them; a spin window is unsafe with other spinlocks held by callers. Revisit with a sleeping lock |
| M-089 | M | (agent) /proc/<pid> cwd, exe, root, maps and fd links readable by any process | fixed b8c2331 | the four Linux asks the ptrace-read question of are refused to a process that is not the owner or root (may_inspect). fs-proc-inspect.txt, red |
| M-090 | M | (agent) execve of a set-id program changed euid but left suid and fsuid | fixed f178c53 | exec sets saved and fs ids to the effective ones, as Linux does; SIGNAL.ELF reads them back through /proc. abi-exec-cred.txt, boot, red |
| M-091 | M | (agent) RLIMIT_AS, MEMLOCK and STACK stored and never asked | fixed a0fc9a6 | mmap, brk and mremap ask AS, mlock asks MEMLOCK, the stack fault asks STACK. mm-rlimits.txt 2 cases, red; mm-rlimit-stack-boot.txt, boot, red (process_calls_failed). Check, 6/6 boots, LTP inotify unchanged (7 pass, the 6 known broken) |
| M-092 | M | (agent) global pools with no per-process quota: POSIX timers, record locks, inotify instances and watches | fixed d1d8b37, 90431f7, 71eb05a | a process may hold at most a quarter of each table. sched-ptimer-share.txt, fs-flk-share.txt, abi-inotify-share.txt, red. The file-description pool is left global: it is Linux's file-max, answered with ENFILE, and NOFILE already bounds a process |
| M-093 | M | (agent) the last release and a new inotify_init1 race over turning notification off | fixed b80aa5c | the switch is made under g_in_lock. No red test: the window needs two cores at one instant |
| M-094 | M | (agent) regular.c's shared offset read and written without a lock | fixed 87d5c22 | one call at a time per description (pos_busy). fs-pos-lock.txt, red. Not covered: getdents' offset, O_APPEND across separate opens |
| M-095 | M | (agent) futex timeout and utimensat overflow for an enormous tv_sec | fixed f15708e | both go through saturating conversions. abi-time-overflow.txt, red. 2026-10-18 (eleventh review): the review's utimensat line is not in the tree - utimensat goes through linux_ns_of - but utime still multiplied, the one call the fix missed; it goes through linux_ns_of too, case 3 red |
| M-096 | M | (agent) VMA pool exhaustion leaves regions and page tables disagreeing | fixed 464a238 | every insert is made before the call reports success and a failed remove says so (VIBEOS_VMA_REFUSED); mmap, brk, mremap and munmap answer ENOMEM. mm-vma-exhausted.txt, red |
| M-097 | M | .f.sh cannot pass; the scanning checks report ok with nothing scanned; sarif-merge.py takes an empty run as clean; no workflow runs the check-*.py; mdBook without a checksum, semgrep on a moving tag, the signed release built with fewer dependencies than the tested one | fixed be918cc, 5ece3ca, 6f5d8ba, 8fad219, 6352b13, 87935dc | a floor of 1200 casts turned check-user-access red; static-checks.sh goes red on a missing python. SAFE in check-user-access matched the line; since 202eea5 a cast must sit inside the copy's arguments, and a planted raw cast beside a copy went red. Afterwards: the corpus's CPU-time workload, flaky on a fast runner, bounded by the clock (c04136c); job and install timeouts after apt held a run six hours (f1f4e52) |
Tenth review (2026-10-13)
| ID | Sev | Finding | Status | Evidence / next step |
|---|---|---|---|---|
| M-098 | M | exFAT: a directory whose FAT chain loops, with no end-of-directory entry in it, was scanned for ever - exfat_dir_scan walked each cluster again from the first through exfat_nth_cluster and stopped only at an end marker or a chain that ended | fixed 1a6fda4 | the scan follows the chain one link at a time with Brent's cycle check, bounded by the volume's cluster count; exfat_nth_cluster refuses an index past it; a broken chain is EIO and exfat_bad_metadata, not ENOENT. test_exfat_cyclic_directory (a loop of two and one of twelve, past the block cache, with a read count); exfat-cycle.txt 3 cases, red (the one without either check hangs) |
| M-099 | M | recvmsg: SCM_RIGHTS descriptors were installed before the control buffer was written, and a failed copy left them in the table under numbers nobody was told | fixed 8a05bb1 | the buffer is judged before anything is installed; a copy that fails after (a sibling's munmap) takes back each descriptor that still names what was put there (linux_fd_withdraw, by description and tenancy); the sender's name is written before the rights. t_unix: a bad buffer and one that faults between judgement and copy, the lowest free descriptor unchanged after both; scm-rights-out.txt, the withdrawal red, the judgement alone NOT RED and why |
Eleventh review (2026-10-18)
The review's R10-1 and its M-101 are one finding, kept as M-101. Two claims
were checked and are not findings: M-096's rollback is in the paths named,
and utimensat goes through M-095's saturating conversion - the overflow it
describes is in utime, which M-095's fix missed (below, under M-095).
| ID | Sev | Finding | Status | Evidence / next step |
|---|---|---|---|---|
| M-100 | M | clone with CLONE_VM and without CLONE_THREAD: page tables shared, region lists and locks separate | fixed (2026-10-21) | The address space's bookkeeping - regions, mm lock, break, mapping cursor, MCL_FUTURE - is its own counted object (vibeos_mm_t, ps->mm); a CLONE_VM child without CLONE_THREAD shares its parent's (ks_mm_share) instead of copying the list, threads share it through the process state, fork and exec make a new one. t_clone_l6: what the child maps the parent can mprotect and does not map over, and the break is one. abi-fork-snapshot.txt red; THREADS.ELF's C6_VFORK runs on it at boot |
| M-101 | M | F_SETFL and FIONBIO each rewrite f->flags whole: two at once on one description lose one update (R10-1 is the same finding) | fixed (2026-10-18) | vibeos_file_change_flags clears and sets in one compare-exchange, and both calls go through it. fs-file.txt case 5 red for the bits it keeps; the race itself is not a case - the host tests have no threads - and is written down as such there |
| M-102 | M | copy_file_range's overlap test summed an offset and the length, so a huge length wrapped the end and a file was copied onto itself | fixed (2026-10-18) | each end is held at the largest offset instead of summed; the host test puts the source above the destination, where the wrapped end landed below it. abi-file-calls.txt case 8 red (NOT RED with the source at 0: the wrapped end still came out above it), and case 3's anchor moved with the line |
| M-103 | M | madvise accepted MADV_POPULATE_READ and _WRITE and populated nothing | fixed (2026-10-18) | vibeos_vmspace_populate brings each page back from swap and, for a write, breaks a copy-on-write share as a store would; a region the access is refused in is EINVAL, a hole ENOMEM, a failure EFAULT - Linux 5.15's answers, probed on the host. abi-mem4.txt case 15 and mm-vmspace.txt's populate case red |
| M-104 | M | mlockall(MCL_FUTURE) was accepted and later mappings were not locked | fixed (2026-10-18) | ps->mlock_future, cleared in linux_procstate_defaults (new, fork and exec all pass there); mmap (EAGAIN past RLIMIT_MEMLOCK), brk, mremap and the stack's growth lock their new pages; munlockall ends it. Registry row 151 DONE. abi-mem4.txt cases 16-18 red; inheritance across fork is not a case, the fake kernel memsets its process states |
Twelfth review (2026-10-19, of HEAD d02273e)
"(agent)" marks a finding the review took from a sub-agent and did not read again itself.
| ID | Sev | Finding | Status | Evidence / next step |
|---|---|---|---|---|
| H-035 | H | mq_timedsend wrote page pointers past a message header's 500-entry array: msgsize_max and msgsize_default took up to 16 MiB, a non-root queue is bounded only by them, and the send loop had no bound of its own | fixed (2026-10-19) | Both tunables, and queue creation for root, stop at LINUX_MQ_MSGSIZE_CAP (500 pages, what a message holds); the send loop refuses an index past the array as well. mqueue-l7.txt case red |
| M-105 | M | NtOpenFile opens with the kernel's authority: no linux_may as openat asks, no search permission on the walk - an existence oracle for any path, and a 0600 PE of another user can be mapped | fixed (2026-10-19) | walked with the caller's credentials (vibeos_path_walk_as) and asked vibeos_cred_may for read, and for execute when FILE_EXECUTE is asked - the questions openat puts, through the personality-neutral layer. nt-openfile.txt two cases red; the case "a file asked for writing" re-anchored |
| M-106 | M | NtMapViewOfSection maps the section's file by path again, so a rename between NtCreateSection and the map maps a different file from the one opened and checked | fixed (2026-10-21) | vibeos_file_still_at_path asks, at section creation and at every map, whether the path still names the handle's mount and node, and STATUS_FILE_RENAMED otherwise. Since W3 step 6 the image is then read by the description's node (vibeos_file_node, getattr and never a lookup by name) and the page cache's identity is given for that node (hw_read_file_node), so a rename between the look and the read changes nothing that is read. What the look still guards is FAT, where a node is a directory slot that an unlink and a create can hand to another file - FAT's own gap, written down in CLAUDE.md. nt-section.txt and fs-description-node.txt red |
| M-107 | M | hw_stack_grow's "fault again" return does not serve a pending TLB shootdown: the same shape b39c801 fixed in ks_mm_lock | fixed (2026-10-19) | hw_tlb_service_flush() before the "fault again" return, as ks_mm_lock's waiters do. Not a case: the arch code is not in the host tests, and the boot reaches this only with a sibling thread in a shootdown while a syscall copies out to an ungrown stack page - the mechanism is b39c801's, whose boot case went red |
| M-108 | M | Kernel stacks have no guard page; the canary (hw_kstack_check) sees an overflow after the damage and only if the base word is written | fixed (2026-10-21) | Kernel stacks live in a region of their own at 256 GiB, whose directory and tables are built with the kernel's before any address space exists and recognised as shared by teardown: each a window of four unmapped guard pages and 16 KiB mapped a page at a time, from 72 windows (tasks and parked stacks). A switch invalidates the incoming stack's pages, so a window reused for another stack is never reached through a stale translation. The double fault runs on an IST stack of its own; a page fault or double fault in a guard panics naming the task. Deviation from the decision: the page fault is not on an IST stack - its handler can wait and be switched away from, and a second fault on the same core would land on the stack the first still uses; the double fault catches the case the page fault cannot push. The canary stays as a second line. mm-kstack-guard-boot.txt: getppid recursing off its stack ends in "kernel stack overflow: a task ran into the guard page" (its first version was turned into a loop by gcc and wedged; the wedge report prints rsp since) |
| M-109 | L | msgctl(IPC_INFO/MSG_INFO) copied linux_msginfo_t uninitialised: two padding bytes of kernel stack to the program | fixed (2026-10-19) | zeroed before it is filled, as shm.c's IPC_INFO is. The test fills the stack with 0xA5 first (dirty_stack): without it the host's stack was zero where the struct landed and the sabotage went NOT RED. ipc-msg-l7.txt case red |
| M-110 | L | (agent) Global tables with no per-process quota: 256 NT objects, 512 futex waiters (futex_waitv up to 128 a wait), 64 pipes of up to 1 MiB each, g_undo[512]; M-092 covered timers, locks and inotify | confirmed and fixed (2026-10-19) | Each was a single process's to exhaust. M-092's rule, a quarter of the table per process: pipes (EMFILE; the machine's own exhaustion is ENFILE now), futex waiters counted by the entry's task's process (ENOMEM), SEM_UNDO entries (ENOSPC), NT objects by their creator (STATUS_QUOTA_EXCEEDED, mapped to ERROR_NOT_ENOUGH_QUOTA as Wine's error.c checks). ipc-pipe-share.txt, abi-futex-share.txt, ipc-sem-undo-share.txt, nt-object.txt, red |
| M-111 | L | (agent) splice: peek, write and read in three holds of g_pipe_lock; room = capacity - pending can wrap after F_SETPIPE_SZ shrinks a pipe | confirmed and fixed (2026-10-19) | The resize itself refuses to shrink below what a pipe holds, so the pipe is never inconsistent; but splice, tee and vmsplice read the size and the count in two holds of the lock, and a shrink between them made the difference wrap - a write then waited with bytes already looked at. Four sites now ask vibeos_pipe_room, one hold. Host test for room; the race is not a case (no threads on the host). The three holds of peek, write and read are by design: the far side's write can wait, and must not under the pipe lock |
| M-112 | L | mq_getsetattr rewrites f->flags whole: the sibling of M-101 | fixed (2026-10-19) | through vibeos_file_change_flags, the old flags taken from the same step. Every write of a description's flags after its creation now goes through that function (grep: the only other is the allocation). Not a case, for M-101's reason: the host tests have no threads |
| M-113 | L | (agent) shmget looks a key up twice without checking permissions again | confirmed and fixed (2026-10-19) | The second look - after the file is made outside the lock, for a key another process created meanwhile - answered with the id, asking neither the permission question nor the size. Both looks go through shm_existing now. The permission question had no host test at all: added, and ipc-shm-l7.txt's new case is red with it (NOT RED before); the size case re-anchored |
| M-114 | L | (agent, uncertain) linux_shm_attached counts regions at offset 0: a munmap of a segment's head may miscount | confirmed and fixed (2026-10-19) | It did: after the head was unmapped the rest started further into the segment and was not counted, so a segment marked for removal was reaped while still mapped (shm_nattch 0 too). An attachment is now a run of regions each continuing the last - a hole in the middle counts twice, as Linux counts a split. Host test; ipc-shm-mm-l7.txt case red |
| M-115 | L | (agent) NT mutant: owned and published before it is initialised | confirmed and fixed (2026-10-19) | nt_ob_create published the named slot and released the lock; nt_create_common then took it again to set the state, so for that moment a mutant created owned was a free one any process could open and take. The state is set inside nt_ob_create, under the lock that publishes it. The race itself is not a case (no threads on the host); "an existing object is reinitialised" moved to nt-object.txt with the code and is red |
| M-116 | L | (agent) sched_target turns a pid into a slot with no sequence check | confirmed and fixed (2026-10-19) | The slot was found under the scheduler's lock and changed after it was let go: an exit and a fork between put another thread there, and the change landed on it (its own credentials were then checked, so no privilege was gained - the wrong thread was changed). sched_apply and sched_setaffinity ask, under the lock that applies the change, whether the slot still holds the thread id named, and say ESRCH otherwise. Not a case: the host tests cannot put an exit between the two |
| M-117 | L | (internal) hw_task_by_pid counted a thread that had exited and not yet been released: a non-leader thread goes through ZOMBIE on its way to FREE with no reaper, and in that moment kill(pid, 0) answered 0 for a process whose leader its parent had already reaped. THREADS.ELF's C6_ABORT saw it once (a thread of it outlived the process), on a boot of W3 step 6 that touched nothing of the sort | fixed (2026-10-22) | The first fix (2026-10-21) skipped a zombie thread in hw_task_by_pid; C6_ABORT failed once more in 12 boots with it in, and pid_dead_thread_skipped stayed 0 - a thread is ZOMBIE only inside the critical section that releases it, so that window does not exist under the lock. The failed boot's log said what did: the second thread's exited line is printed long before its slot changes state, so kill found it RUNNING on its way out of the exit_group - after waitpid had reaped the leader, which it did for any zombie leader. Linux reports a leader only once its group is empty (delay_group_leader). linux_wait_child now passes over a zombie leader while another thread of it is alive, and exit tells the parent - wake and SIGCHLD - from whichever finishes last, the leader or its last thread (hw_notify_parent). The window is reached on every boot: exit_leader_before_threads and exit_parent_told_late on the [TASKS] line were 1 and 1 (once 2 and 2) in all of 12 boots, 12/12 clean. The fifty yields C6_ABORT gave the pid after the reap were what hid it to one boot in fifteen; they are gone, and with them gone, removing the wait's check goes red on the first boot (threads-leader-reap-boot.txt), and on the host (abi-processes.txt case 7, t_processes) |
| M-118 | L | (internal) The corpus workload bb-ps saw its parent - the shell, blocked in wait4 for ps - as R where Linux and every other boot say S: corpus_answers_differ:bb-ps, once in about seventy boots on 2026-10-21, in the boots that closed W3 | open | The shell blocks in linux_wait_child and is made READY by a child's exit or a signal; what woke it while ps read /proc is not known. Nothing of the workload is W3's. Next: have /proc's stat say which wake made a waiter READY, rather than guess from one sample |
| M-124 | M | (internal) A silent boot wedge: a core in ahci_cmd's wait for PxCI to clear, the other cores idle behind the disk, after svc-reclaim's RECLAIM_SWAP; 3 of 6 boots under host load about 6 on 2026-10-22, 0 of 6 for the same binary on a quiet host and for its parent. The wait was bounded by twenty million turns, and once the wait halts - interrupts on, after 1024 turns - each turn lasts until the next interrupt, a tick: the bound was two days, so a command QEMU never completed was never timed out, and the gate saw a silent machine at a misleading phase (busybox_cat) | mitigated, cause open (2026-10-22) | The wait is bounded by time now, five seconds of timer ticks; a command that runs out prints [AHCI] command timed out with CI, IS, TFD, SERR, CMD and the IRQ count, is counted, and reaches the gate as io_timeout instead of a wedge. ahci-timeout-boot.txt: a late write never seen to complete times out at ticks=0x1f5 with the guest still talking. Why QEMU leaves a command uncompleted under host load is not known; the next occurrence prints the port's state, which is where to start |
Thirteenth review (2026-10-22, of HEAD 74df400)
| ID | Sev | Finding | Status | Evidence / next step |
|---|---|---|---|---|
| M-119 | M | TCP accepts ACKs up to two sequence numbers beyond buffered data even when no SYN/FIN is outstanding. tcp_ack_data clamps the byte count to tx_len but stores the unvalidated ACK in snd_una, which can move it past snd_nxt and break flight-size accounting / subsequent sends | fixed (2026-10-22, c8f79ba; GitHub #14): the bound is snd_nxt, host test test_inet_input_review, sabotage in net-l5-inet.txt | kernel/net/inet.c:1947-1964, caller tcp_input:2131-2133. Issue #14. Reject ACKs beyond the sequence space actually sent; only account for SYN/FIN sequence numbers when outstanding. Add cases for snd_nxt+1 and snd_nxt+2 |
| M-120 | M | IPv4 input checks only the 13-bit fragment offset (0x1FFF) and misses the MF flag (0x2000). A first fragment with offset zero and MF set is passed to transport handlers without reassembly; TCP checksum validation then covers only the fragment payload and can accept the partial segment | fixed (2026-10-22, c8f79ba; GitHub #15): MF and the offset both refused, host test and sabotage as M-119 | kernel/net/inet.c:2228-2285, fragment check at ~2245. Issue #15. Drop packets with MF set as well as nonzero offset (while allowing DF); test a first fragment with MF set and a checksum matching the fragment bytes |
| M-121 | L | DNS accepts the first A record in the answer section without validating its owner name or RR class, then caches that address under the outstanding query name. A response can echo the right question yet supply an unrelated A owner; the resolver accepts it after M-007's source/port/id/question checks | fixed (2026-10-22, c8f79ba; GitHub #16): owner name and class IN checked, a CNAME chain followed; test_inet_dns_answer_review | kernel/net/inet.c:1352-1395. Issue #16. Parse and validate answer owner/class; only accept the queried name or a validated CNAME chain. Add a response with the right question but unrelated A owner and assert it is not cached |
| M-122 | L | fallocate() reports success without reserving the requested range: mode 0 only extends visible size via truncate, and FALLOC_FL_KEEP_SIZE does nothing. Future writes can still fail for lack of space | known gap, documented (reviewed 2026-10-22). Not new: the comment above linux_sys_fallocate says the size is guaranteed and the space is not, and linux_syscalls.def row 285 is PARTIAL for exactly that. The remedy proposed below - EOPNOTSUPP for mode 0 - would break the programs that call fallocate to extend a file, which is what the size guarantee is for; reserving space needs a filesystem that can promise it. Lowered M to L | kernel/abi/linux/fs.c:1045-1082. Issue #17. Until the filesystem layer can reserve the range, reject both modes with EOPNOTSUPP rather than silently succeed; test both modes and, once supported, verify actual reservation and KEEP_SIZE semantics |
| M-123 | L | timerfd_settime() accepts TFD_TIMER_CANCEL_ON_SET without TFD_TIMER_ABSTIME, although Linux rejects that invalid flag combination with EINVAL | withdrawn (2026-10-22): not a defect. Linux accepts the combination - a probe on the host's kernel (5.15) answered 0 for timerfd_settime(fd, TFD_TIMER_CANCEL_ON_SET, ...) on CLOCK_REALTIME and on CLOCK_MONOTONIC; CANCEL_ON_SET only takes effect beside ABSTIME on a settable clock, and is otherwise ignored, as VibeOS ignores it. The fix proposed below would make VibeOS refuse what Linux accepts | kernel/abi/linux/events.c:108-134. Issue #18. Reject CANCEL_ON_SET unless ABSTIME is also set; add an ABI test for the invalid combination |
| M-125 | H | vma_change_locked read cur->next before vma_try_merge, which can absorb that successor into cur and free it; the walk continued from the freed descriptor, and prev never advanced. Reached by any mprotect or NtProtectVirtualMemory spanning regions the change makes alike (review of 2026-10-26, read in the source) | fixed (2026-10-26, 55f4cf0): the walk continues from the descriptor that survived; a vma host test protects five alternating regions into one, and mm-vma.txt puts the old order back and goes red | kernel/mm/vma.c, vma_change_locked |
| H-036 | H | NT section views had no write ceiling: a handle with SECTION_MAP_READ alone mapped a read-only view and NtProtectVirtualMemory then made it PAGE_READWRITE, storing into shared pages. H-033's class in the NT personality (review of 2026-10-26) | fixed (2026-10-26, 8b45ad2): a shared view through a handle without SECTION_MAP_WRITE, or of a section that is not writable, carries VIBEOS_PROT_NOWRITE; nt_abi test, nt-section.txt case red | kernel/abi/nt/section.c, map_data_view |
| M-126 | M | A failed fork leaks a kernel-stack window, 72 in all, after which every fork is ENOMEM (review of 2026-10-26) | already fixed (#141): every failure branch of linux_sys_fork after ks_alloc_kstack calls ks_free_kstack, and a thread's clone has no failure exit after the allocation. Checked in the source 2026-10-26 | kernel/abi/linux/proc.c |
| M-127 | L | No per-process quota on the 2048-descriptor VMA pool: one process can take every region (review of 2026-10-26, from its sub-agents) | fixed (2026-10-26, 738faa7): an address space holds at most a quarter of the pool, as the futex and record-lock tables bound a process; past it an insert or split is refused and every caller answers ENOMEM. vma host test, mm-vma.txt case red | kernel/mm/vma.c pool |
| M-128 | L | Private futexes are keyed by process state, not by address space: two processes sharing memory through CLONE_VM without CLONE_THREAD do not meet on a private futex (review of 2026-10-26) | fixed (2026-10-26, 071e54c): a private word is keyed by the address space (futex_space); linux_abi test with a CLONE_VM child, abi-futex-share.txt case red | kernel/abi/linux/futex.c, linux_futex_key |
| M-129 | L | kill may pick a thread that has already exited to deliver a process signal (review of 2026-10-26, from its sub-agents) | fixed (2026-10-26, df84bb0): confirmed - a leader that called pthread_exit is a zombie while its threads run, and kill raised the signal on it. A zombie is passed over now; linux_abi test, abi-host-handlers.txt case red | kernel/abi/linux/sig.c, linux_process_thread |
| M-130 | L | SO_REUSEPORT lets a socket of another user share, and so take, a bound port: Linux requires the same effective uid (review of 2026-10-26) | fixed (2026-10-26, a0ffa7b): the stack keeps the effective uid that asked for SO_REUSEPORT and shares a port only between equal ones; kernel test, net-l5-inet.txt case red | kernel/net/inet.c:625 |
| H-037 | H | NtQueryDirectoryFile read its mask through the UNICODE_STRING's buffer pointer unjudged: vibeos_uaccess_copy is fault-safe, not range-checked, so a kernel address filtered the listing - an oracle on kernel memory (v7 review, 2026-10-26) | fixed (6128571): every copy in kernel/abi/nt goes through nt_copy_in/nt_copy_out, which judge the range; check-nt-user-copy.py (static-checks) fails any other; the row declares the mask | kernel/abi/nt/dispatch.c, file.c |
| H-038 | H | The W4 paths that change a directory asked nothing: disposition and DELETE_ON_CLOSE (unlinked with the kernel's authority), rename (neither directory, replaced its target), NtDeleteFile (no sticky bit), FileBasicInformation (any file's times); new files were root's. M-105's class again (v7 review) | fixed (088ab90): the add/remove/own questions live in cred.c, shared by Linux and NT; DELETE and WRITE_ATTRIBUTES judged at open, rename asks both directories; check-nt-fs-permission.py fails an NT function that mutates the filesystem unasked (c17f12a) | kernel/sched/cred.c, kernel/abi/nt/file.c |
| H-039 | H | No NT file service asked what its handle was granted: nt_file_get read the descriptor table straight, so a handle for attributes read data, one opened to read truncated or set times, a duplicate with fewer rights had them all (v7 review) | fixed (6128571): nt_file_get takes the rights a service needs and goes through nt_handle_ref; each service names them | kernel/abi/nt/file.c |
| M-131 | M | v7 review's mediums in the NT file services: alignment gaps of directory entries carried the kernel's stack; a mask of stars was exponential; every NT process shared one quarter of the lock table (pid 0); FILE_CREATE followed a dangling link; a rename's name was not case-insensitive; position and end of file took values past 2^63; touching locks were joined so an exact unlock failed; walks ignored the process's root | fixed (29969ed, 088ab90, acf4c57, 7f042ea), each with a host test and a case that goes red; the root is latent until L8's chroot | kernel/abi/nt/file.c, kernel/fs/filelock.c |
| M-132 | M | A Linux rename of a file left its descriptions naming the old path, and the release honouring DELETE_ON_CLOSE removed by path - the newcomer at the old name went; directory descriptions never honoured the mark at all (v7 review, M-080's family) | fixed (c3821c4): a file's descriptions follow a rename; the release removes only a name that still names its file, by rmdir for a directory | kernel/abi/files/regular.c, kernel/abi/linux/names.c |
| M-133 | L | The perf sweep's five notes (v7 review): the task generation moved before the state it announces; a tmpfs truncate stopped short of a middle page a failed write left; an initrd's suffix ran past the 4 GiB cut; a FAT table write failing half-way kept the chain cursor; the timer drained up to sixteen TLB-quarantine batches with interrupts off | fixed (db9e0fa, 71b92e9, 7f87e44, cd245b8, 61d8bae). Two without a case, written down why: the generation's window is between cores, and the FAT cursor cannot be reached through the VFS | see the commits |
Fourteenth review (2026-10-22, GitHub issues #19-#78)
Filed as GitHub issues; #14-#18 are M-119..M-123 above. Every claim about what Linux does was probed on the host's 5.15 before it was acted on.
| Issue | Finding | Status |
|---|---|---|
| #19 | ext2 read narrowed a 64-bit block index to 32 bits | fixed 9d8395c - no host fixture holds a file that large: no failing test |
| #20 | PE32+ ImageBase + RVA wrapped past 2^64 | fixed 00e92bd - section end checked; boot-memory-map-core.txt red |
| #21 | ISO9660 extent + offset narrowed to a low sector | fixed 9d8395c - as #19, no failing test |
| #22, #28, #29, #32, #33, #34, #38, #39 | sched_setattr size 0; clock_nanosleep on the process CPU clock; sendmmsg vlen; unknown semop/msgsnd/msgrcv/shmat flags; sigaltstack SS_ONSTACK; rt_sigqueueinfo si_signo | withdrawn - Linux 5.15 accepts each, probed on the host (abi_probe.c); closed on GitHub |
| #23 | O_APPEND serialised only per description | fixed c9511cb - a per-file write turn; needs two threads, not host-testable |
| #24 | getdents moved the offset without the position turn | fixed 4ae5718 |
| #25 | flock range: negative length applied before the start was judged | fixed f416f98 - host test + sabotage |
| #26 | sync_file_range called fsync and ignored its arguments | fixed a16fcaa - host test + sabotage |
| #27 | epoll ET marked seen on a failed copy | fixed afa9b4c - only a sibling munmap reaches it |
| #30 | SEM_UNDO judged per operation, not per call | fixed 1bb266b - host test + sabotage |
| #31 | msgrcv negated LONG_MIN | fixed b19f2e5 - host test + sabotage |
| #35 | mq_notify dropped when the process closes any queue descriptor | not as filed - Linux 5.15 drops it on any close by the owner (probed); this kernel kept it across a dup's close, fixed the other way in 3af08a1 |
| #36, #41 | kill: INT64_MIN negated; signal 0 to a group asked about the caller | fixed c5c9a86 |
| #37 | prlimit64 on another process wrote limits without its files_lock | fixed 23fb9aa |
| #40 | setpgid moved any process of the session | fixed fbad98b - did_exec in the identity; host test + sabotage |
| #42 | rt_sigaction / delivery read and wrote the disposition unlocked | fixed bec9e93 - under files_lock |
| #43, #44, #45 | clone3: wrapping stack, pidfd leaked on a failed copy, pidfd made after the child ran | fixed 5d910b2 - #43 host-tested; #44/#45 need a race |
| #46 | signalfd mask read plainly, stored atomically | fixed 2f9428e |
| #47, #50, #56, #58, #61, #68 | socket options: timeout overflow, unlocked flag word, FORCE capped, SO_DEBUG unprivileged, connect to broadcast, SO_DONTROUTE ignored | fixed b452671 - each probed on 5.15 first; host tests + sabotage |
| #48, #49, #51 | netctl ping/lookup shared by callers; start failures ignored | fixed 6b272e8 |
| #52, #53, #54, #55 | kill once per thread; to a blocking leader; tid probe; tgkill order | fixed e1c7ef6 - host tests + sabotage |
| #57, #59 | SO_REUSEPORT on AF_UNIX, SO_PASSCRED on AF_INET | not reproduced - Linux 5.15 accepts both (probed); later kernels restrict them. Left to decide |
| #60 | SO_REUSEPORT stored and never told to the stack | fixed 134d773 - the same-user half is not done |
| #62, #63, #69 | ICMP checksum; ping reply sequence; DNS TC/RCODE | fixed c8f79ba |
| #64 | DHCP renew/rebind sent as a client without a lease | fixed 2407927 |
| #65, #66 | UDP and TCP demultiplexed by port alone | fixed d3c812b |
| #67 | DNS retry after ARP miss | withdrawn by its author |
| #70 | int arguments read as 64 bits | fixed 032efcf - descriptors narrowed where they are looked up |
| #71 | sched_setaffinity accepts a mask under eight bytes | withdrawn by its author: a one-byte mask suffices for eight CPUs |
| #72, #73, #76, #77 | wait4/waitid dropped an unwritable rusage; setpgid/getsid tested 64 bits for zero; mprotect took unknown bits; poll ignored RLIMIT_NOFILE | fixed 324fcc5 - each probed on 5.15; mmap's unknown bits and select's nfds Linux accepts, and so does this |
| #74 | MADV_DONTNEED leaves a private file page as the program wrote it | known gap, open: a private file region keeps no file or offset - its pages are copied at mmap - so there is nothing to read back. Needs a file reference in the region, through fork, split and teardown |
| #75 | MLOCK_ONFAULT / MCL_ONFAULT lock eagerly | approximation, documented: Linux mappings are populated at mmap here, so every page exists to lock; eager is more than ONFAULT asks, visible only to mincore over swapped pages |
| #78 | failed mremap growth under MCL_FUTURE keeps its region | not a defect: vibeos_umem_unmap removes the region; t_mremap_lock_rollback shows it, and a sabotage doing what the issue describes goes red |
| #79-#121 (performance) | scans on hot paths, busy waits, missing indexes | filed 2026-10-22/23; taken a few at a time, the rest open on GitHub |
| #92, #93, #120 | TLB quarantine scanned whole after placing and on empty ticks; full swap map scanned | fixed 101d554 |
| #98 | real-timer table walked every tick with none armed | fixed 293890d - sabotage in sched-ptimer.txt |
| #102 | quarantine drain stopped at 32 frames | fixed ffb06d9 |
| #109, #116 | boot DHCP and keyboard waits counted in pauses | fixed d5c2316 - bounded by ticks, the count kept as a backstop |
| #96, #95, #86, #108, #115, #106, #107, #119, #117, #105, #99, #100, #97, #110, #90, #132, #133, #138, #139, #134, #118, #94, #137 | performance fixes, each one commit | fixed 2026-10-23..25 - 6/6 boots on each batch on a quiet host |
| #135, #141 | frame pool above the identity map; a failed fork kept its kernel stack | fixed 1dbf951, 2026-10-25 |
| #17, #75 | fallocate reserves nothing; ONFAULT locks eagerly | closed as documented approximations |
label perf-phase (30 issues) | the rest of #74, #79-#131, #136, #140 | deferred to a performance phase after W4: each changes a shared mechanism (readiness wakeups, cache locking, ring buffers, the boot order) and none is a defect; to be taken with the benchmarks #85 asks for |
Found internally while verifying the above
| What | Status | Evidence / next step |
|---|---|---|
ILLEGAL from=running to=running by=hw_task_exit, the slot a core's idle task (cr3_by=create_idle) - one boot in six while validating H-031..H-034, and the same line in .boot-evidence/fail-20260911-143808-boot7.log | instrumented, open (2026-10-07, 2d30753) | hw_task_exit falls back to cpu->idle_task when the picker finds nothing and sets it RUNNING; the idle task was RUNNING already. hw_schedule marks an idle it leaves READY, so the idle was RUNNING somewhere else or left so by another path. Hypothesis to test first: the run queue can hand one core's idle to another core's picker, which would put two cores on one idle stack. Log kept as wedge-serial-2.log of that run. 2d30753 re-reads the core after cli (a task migrated between picking cpu and disabling interrupts would fall back to another core's idle), counts it as exit_migrated, panics if the core's current task is not the dying one, and prints every core's current and idle when the successor is already RUNNING. Twelve boots clean with exit_migrated=0: the migration hypothesis is not confirmed. Read the new line on the next occurrence |
hw_task_exit makes next current with interrupts possibly on; a timer there saves the dying task's kernel frame as next's context | fixed (commit "core: exit switches tasks with interrupts off") | must-be-zero counter exit_switch_irq_on red at 4 per boot, 0 with cli; twelve boots 12/12. It was not the cause of the four-worker crash family, or not the only one: the same signature recurred after the fix (.boot-evidence/fail-20260911-154006-boot7.log, rip 0x405b72, free-page-poison panic). The window was real and stays closed; the family is open again. boot_repeatability.md |
Linux gcc / Debug red on three runs running, each differently: a kernel #GP in vibeos_file_get from execve's linux_fds_copy (a table entry held a non-canonical pointer), mm_frames_leaked=9, a wedge; locally a kernel jump into .bss (g_bc) after an NT program exited | fixed (2026-10-18) | The exec of a Windows program ran past the base of its 8 KiB kernel stack in the unoptimised build: -fstack-usage put hw_pe_map_images at 2,880 bytes (two vibeos_pe_image_t locals, grown by VIBEOS_ELF_MAX_SEGMENTS 15 -> 24 in W2), hw_proc_create 2,624 and linux_sys_execve 1,632 on one path, with the cache and the disk below them. Stacks have no guard page, so the overflow wrote into the next frame and every symptom was somewhere else. A canary at each kernel stack's base, checked every tick (hw_kstack_check), turned the Debug boot from three different failures into kernel stack overflow at /win/hello.exe's exec, 3 boots of 3 (.boot-evidence/kstack-overflow/); the image descriptions are static under the locks their paths already hold. Evidence of the CI run in .boot-evidence/ci-39033105778/ |
THREADS four-worker crash family (new thread faults in musl start, stack is the free-page poison) | open - and the probe masks it | with probes in the kernel: 0 crashes in 70 boots; without: 2 in 36 (probe-20260911-174620-boot1, -boot35). The quiet probe logged nothing and still masked it; what it kept was a page walk and a user read in hw_task_load_cpu_state when a thread starts from its initial context - so the window is at a thread's first start. The gate now snapshots every core from the QEMU monitor the instant the poison marker appears - no guest-side work (commit "gate: snapshot every core at a THREADS thread-fault"). Read the snapshot on the next crash. boot_repeatability.md 2026-10-09: dormant. The nightly's Boot Smoke x5 ran 145 boots from 2026-09-11 to 2026-10-09 with one failure, net_sock_fd_aba (fixed in 4a216f1), and every CI push boots four more; the gate fails on this signature and it never appeared. Nothing found by reading in this pass either. Kept open, not closed: absence in boots is not a mechanism, and the instrumentation stays so the next occurrence arrives described |
| Boot gate hung 48 minutes on a guest that had panicked | fixed (commit "gate: the serial pump returns while a guest is still printing") | the serial pump looped until the socket was empty, so a guest printing without pause - faulting in its own panic path - kept it from returning, and the deadline and silence budget, checked outside it, never ran. drain_available returns after 0.25 s; its self-test hangs with the budget removed |
rmap_mismatch=1 with rmap_audit_torn=0 | partly fixed, recurred - reopened (commit "mm: fork's audit counts the references the TLB quarantine holds") | an unmap removes the holder at once and the owner only when the quarantine drains, so a still-mapped frame has one owner more than holders, with the owner count still. The audit now adds references the quarantine actually holds; any other disagreement still counts. Host test red first. But rmap_mismatch=1 with torn=0 recurred on a kernel that includes this fix (probe-20260911-200632-boot9), so the quarantine was a real partial cause and not the whole one. Open. Next: when the audit finds a mismatch, log the frame and its holders, owners and quarantined count, so the disagreement is described rather than only counted 2026-10-09: dormant. The nightly's Boot Smoke x5 ran 145 boots from 2026-09-11 to 2026-10-09 with one failure, net_sock_fd_aba (fixed in 4a216f1), and every CI push boots four more; the gate fails on this signature and it never appeared. Nothing found by reading in this pass either. Kept open, not closed: absence in boots is not a mechanism, and the instrumentation stays so the next occurrence arrives described |
A boot wedges in the reclaim load with one core in the disk driver (mmio_read under adapt_submit, AHCI, or virtio_blk_rw_n) waiting for a swap write that does not complete, the others idle | open, seen only on a loaded host (2026-10-13) | Seen on L6 step 4's first run with a game on the host (0/6), and on 2026-10-13 at 0/6, 1/6 and - for the parent commit 38cfe29, 6/6 the day before - 3/12. All of the 2026-10-13 runs had two orphaned vibeos_kernel_tests spinning on two of eight cores, left by a hung sabotage case (sabotage.py now kills the group); with them gone the same tree booted 5/6, the sixth still printing at the deadline. So the stall follows host load. Whether a guest disk write can wait without bound when the host is slow is still open: time each swap write in the driver and print the longest |
| sched_setaffinity's caller still off its allowed cores after a second; THREADS.ELF's C6_SCHED fails about one boot in eighteen (exit 11, three seconds) | fixed (2026-10-19) | an INFO line (code 63) names what each allowed core runs. On most boots: another user task, NORMAL or IDLE class - the caller is released at its slice's end and waits in that core's queue. Not a refusal; a latency of the picker on a busy boot. Next: log the caller's virtual time against the core's runnable set when it fires. 2026-10-19: two boots of six on the twelfth review's fixes (HEAD eb95f74, host loaded at 38%) with this signature - mask applied, thread not moved in 3 s - and suspected of M-116, which touched sched_setaffinity. Checked: the parent d02273e 6/6 and HEAD again 6/6, both on an idle host; M-116 adds an ESRCH that would fail the call, where the logs show the mask set. The known latency, seen at a higher rate under load. Fixed 2026-10-19, from the INFO line's own numbers: core 1 was running another NORMAL task with less virtual time than the pinned one, and the picker orders by virtual time across the machine - the pinned task waited behind it, and the core it was leaving kept it for the rest of its slice. A task moved to cores it is not on is now placed at the lowest virtual time of what those cores run and wait for (vibeos_sched_policy_place, as Linux places a migrated task; never raised, not applied when already on an allowed core), and a core drops a task whose affinity excludes it at the next tick. With three busy loops on the host: before, 1 C6_SCHED failure and 3 boots waiting over a second in 6; after, 0 and 0 (those boots then wedged in the known AHCI swap-write stall the load causes). Idle host 6/6. sched-l6-policy.txt case red |
The capability/MAC engine (kernel/core/policy.c, kernel/core/security.c) is called only by vibeos_kmain's own init (vibeos_policy_init) and by kernel/proc/process.c's token get/set - never from the real syscall path | scheduled: docs/abi/ L9 (2026-10-09) | Verified: greping vibeos_policy_|vibeos_security_ against kernel/abi/linux/*.c and kernel/arch/x86_64/*.c (the real Linux ABI, the only path that serves a program) returns nothing. This is the architectural fact already on record - vibeos-arch-reality notes networking/security "still host models" as of the portable-kernel bring-up, and CLAUDE.md's own "BOOT_OK is printed after userland has finished" section says vibeos_kmain and the portable subsystems it owns run after the arch layer has already done all its work, "which is also why it can be host-tested and yet have no runtime role." process.c/policy.c/security.c are that portable model: a capability/MAC state machine with host tests, never consulted by hw_sys_*. Wiring real enforcement into every live syscall is a security-critical feature, not a bug fix - the scope of a phase, not a line. Left open rather than rushed: recorded here so the gap is legible instead of silently believed closed, and picked up as its own phase when the plan reaches it. That phase is L9 in docs/abi/phases.md - capabilities on top of L2's credentials, seccomp, Landlock - which is where it now lives |
Order of work
Incoming findings first - the user's stated priority - and the core plan is not dropped:
- H-011 is done, and its audit found the read-path row above - to be fixed with M-009 and M-010 as one filesystem-bounds change. 0b. H-012 is done.
- The network findings H-008, M-007 and H-009 are done, and so is the exit-window fix - which did not end the four-worker crash family; that family is open again.
- H-010 + H-003 are done - and with them C5's "real exception table" item.
- Every review finding is closed. What remains open is internal: the four-worker crash family.
- Core plan C5, remaining: a must-be-zero check for
hw_procstate_t. (fork against its own threads was closed by the address-space lock; mprotect's half of it by M-071; descriptors per thread by M-072.) - The two open tooling/defect rows above.