Stage D: parked-idle VMs are killable, KILL unregisters; fix uninitialized pending_reap (FABRIC-3.8 §3.5, G6)
sk_vm_parked_idle(): saved context + exactly its own IDLE frame. capsule_vm_kill_refusal() is the single kill rule, shared by capsule_vm_kill() and the KILL word: refuse SWITCHED_OUT mid-task, parked with mail pending, or the VM running; allow parked-idle. capsule_vm_kill() now unregisters the switch slot (G6). Finding fixed: vm_registry_alloc() never initialized pending_reap (kmalloc does not zero); C-b's reap_pending walks every participant, so garbage reaped a healthy VM (seen live before any KILL). Now zeroed at alloc. Probe (reverted): baseline KILL refused, slots 3->4->4 (FAIL, proves G6); with Stage D dprobe parked idle, KILL dead, slots 3->4->3, PASS on amd64/ aarch64/riscv64. Acceptance: dict_hash unchanged, relay 3, idle growth 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
06bcab504d
commit
604ad7aadc
@@ -38,6 +38,7 @@
|
||||
#include "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-2.md §H.12 step 5 */
|
||||
#include "starkernel/timer.h" /* timer_calibration_record()->vm_mode -- FABRIC-2.md §I.5 CONTRIB trust tier */
|
||||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 4 -- capsule_vm_force_reap() */
|
||||
#include "starkernel/vm/switch.h" /* FABRIC-3.8.md §3.5 -- sk_vm_parked_idle(), current VM */
|
||||
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.2 -- sk_hermes_channel_subscribe() */
|
||||
#include "word_source/include/framebuffer_words.h" /* FABRIC-3.6.md task 1.8 -- register_framebuffer_words() */
|
||||
#include "vm.h"
|
||||
@@ -204,6 +205,14 @@ static VMRegistryEntry *vm_registry_alloc(void) {
|
||||
* out of scope" for making this dynamic */
|
||||
node->entry.vm_ptr = (void *)0;
|
||||
node->entry.stadium_patron_cell = STADIUM_CELL_NONE;
|
||||
/* FABRIC-3.8.md Stage D finding (2026-09-23): was never initialized --
|
||||
* kmalloc does not zero, so every born VM started with whatever
|
||||
* pending_reap the allocation held. Latent while only the old Stage 3
|
||||
* checkpoint read it (on a timer-picked target); live once Stage C-b's
|
||||
* sk_vm_switch_reap_pending() began walking every participant at every
|
||||
* safe point -- a garbage non-zero reaped a healthy VM (seen live:
|
||||
* "REAP: dprobe tombstone reclaimed" before any KILL). */
|
||||
node->entry.pending_reap = 0;
|
||||
for (i = 0; i < VM_NAME_MAX; i++) node->entry.name[i] = '\0';
|
||||
node->next = (void *)0;
|
||||
|
||||
@@ -409,6 +418,33 @@ static void dispatch_init_forth(void *vm_ctx) {
|
||||
* VM Kill
|
||||
*===========================================================================*/
|
||||
|
||||
/* FABRIC-3.8.md §3.5 (Stage D, 2026-09-23). See the header. */
|
||||
const char *capsule_vm_kill_refusal(const VMRegistryEntry *entry) {
|
||||
if (!entry || entry->state == VM_STATE_DEAD) return (const char *)0;
|
||||
|
||||
/* A VM parked mid-task has a live frame on its native stack that must
|
||||
* be resumed; a VM parked at its own trampoline with nothing in
|
||||
* progress (sk_vm_parked_idle(): exactly its own IDLE frame) has only
|
||||
* the trampoline loop there, which nothing ever needs back. It must
|
||||
* also have no mail waiting: those messages hold their senders' Stadium
|
||||
* heat, and nothing would ever drain them once the VM is gone. */
|
||||
if (entry->state == VM_STATE_SWITCHED_OUT) {
|
||||
if (!sk_vm_parked_idle((const VM *)entry->vm_ptr)) {
|
||||
return " refused -- switched-out mid-task (context parked)";
|
||||
}
|
||||
if (sk_hermes_pending_count(entry->vm_id) > 0) {
|
||||
return " refused -- parked with mail still pending";
|
||||
}
|
||||
}
|
||||
|
||||
/* The VM physically running can't free its own stack out from under
|
||||
* itself -- a switch participant can now be the one executing KILL. */
|
||||
if (entry->vm_ptr && entry->vm_ptr == (void *)sk_vm_switch_current_vm()) {
|
||||
return " refused -- it is the VM running";
|
||||
}
|
||||
return (const char *)0;
|
||||
}
|
||||
|
||||
int capsule_vm_kill(const char *name) {
|
||||
VMRegistryEntry *entry;
|
||||
VM *vm;
|
||||
@@ -456,12 +492,17 @@ int capsule_vm_kill(const char *name) {
|
||||
* here would leave that saved SP pointing into freed memory, a real
|
||||
* use-after-free the moment anything ever tried to resume it. Refuse
|
||||
* rather than silently corrupt; the caller can retry once the VM has
|
||||
* been switched back in and reaches a normal LIVE state. */
|
||||
if (entry->state == VM_STATE_SWITCHED_OUT) {
|
||||
console_puts("KILL: ");
|
||||
console_puts(name);
|
||||
console_println(" refused -- switched-out (context parked)");
|
||||
return -1;
|
||||
* been switched back in and reaches a normal LIVE state. Stage D
|
||||
* narrows this to what actually needs resuming -- see
|
||||
* capsule_vm_kill_refusal(). */
|
||||
{
|
||||
const char *why = capsule_vm_kill_refusal(entry);
|
||||
if (why) {
|
||||
console_puts("KILL: ");
|
||||
console_puts(name);
|
||||
console_println(why);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
vm_id = entry->vm_id;
|
||||
@@ -482,6 +523,12 @@ int capsule_vm_kill(const char *name) {
|
||||
sf_free(vm);
|
||||
}
|
||||
|
||||
/* Stage D (§3.5): same generic cleanup capsule_vm_force_reap() already
|
||||
* does -- a dead VM is no longer a switch participant. Before this,
|
||||
* an ordinary KILL left its slot behind (G6), to be walked by every
|
||||
* decision and never freed. No-op for a VM that never registered. */
|
||||
sk_vm_switch_signal_unregister(vm_id);
|
||||
|
||||
entry->vm_ptr = (void *)0;
|
||||
entry->state = VM_STATE_DEAD;
|
||||
for (i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
|
||||
|
||||
Reference in New Issue
Block a user