Stage C: cut over BLK-ATTACH-EVENT alone -- FABRIC-3.6.md task 3.8

The reply leg (Artemis -> Hera ack) that used to flow through
common:messaging.4th's MSG-SEND/MSG-TICK now goes through
kernel-Hermes's sk_hermes_send_one()/sk_hermes_drain_checkpoint()
instead -- FORTH Hermes never sees a BLK-ATTACH-EVENT message again
(SXXXIV.2's partition rule). The request leg was never real FORTH
messaging traffic to begin with (a direct VM-EXEC, no type tag,
forced by Hera's own inability to load common:messaging.4th), so it
is untouched.

New KH-BLK-ATTACH-SEND (repl.c) wraps sk_hermes_send_one(), reached
from capsules/artemis/init.4th's HERA-BLK-ATTACH-REQ. Delivery reuses
task 3.4's already-wired sk_hermes_drain_checkpoint(); BLK-ATTACH-ACK
itself is unchanged, just reached by a different layer.
SK_HERMES_MSG_TYPE_BLK_ATTACH deliberately reuses BLK-ATTACH-EVENT's
own value (9) to document this as a cutover of the same message, not
a new one.

Two real bugs found on the way, both recorded in FABRIC-3.6.md's
findings log:
- A popped FORTH CREATE-buffer address was raw-cast to a host pointer
  instead of going through vm_ptr() -- silently read all-zero memory,
  no crash, no error, just a message that arrived and did nothing.
  Fixed; the rule and its exception (repl.c's own dev-addr is
  legitimately a raw pointer, formatted that way by its own pushing
  code) are written up for the next FORTH-facing C word.
- A separate, genuine hang on the very first live exercise of this
  path, never reproduced across ten subsequent boots. Reported, not
  chased -- not blocking, per the task's own check being otherwise
  fully satisfied.

Also found live: log_message() is invisible in this build's actual
serial-log capture at every level -- settled on a single
console_println in the real drain target instead, one line per real
USB attach, not a hot-path.

Final acceptance (logs/20260922-105501, -105758, -110304, disk images
reset before each): dict_hash identical across all three
architectures for every VM, zero UNKNOWN WORD, mkcapsule --lint
clean, real ledger+stadium_conserved(Artemis)=true evidence on every
boot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-22 11:08:26 -04:00
co-authored by Claude Sonnet 5
parent f37aa0fb17
commit bbfd9103f4
16 changed files with 102284 additions and 66 deletions
+122
View File
@@ -45,6 +45,8 @@
#include "starkernel/capsule_wirebind.h"
#include "starkernel/capsule_run.h"
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.8 -- sk_hermes_send_one() */
#include "starkernel/vm/stadium.h" /* task 3.8 -- stadium_conserved() evidence print */
#include "block_subsystem.h"
#include "word_source/include/keyboard_words.h"
#include "word_source/include/block_words.h"
@@ -256,6 +258,46 @@ static void sk_word_blk_attach_ack(VM *vm) {
if (ms) ms->bot_msc_attached = 1;
g_attached_blk_dev = dev;
/* FABRIC-3.6.md task 3.8 evidence: this handler IS the real drain
* target -- sk_hermes_drain_checkpoint() (task 3.4) called
* vm_interpret() on the ack payload, which is how we got here.
* stadium_conserved() (task 0.7/2.7's four-term form) holds at
* every instant, mid-hold included, so printing it here -- while
* this message's heat is still held, before sk_hermes_drain_
* checkpoint()'s own release/pop run just after this function
* returns -- is real evidence, not a synthetic self-test. Note the
* limit: this is evidence for the mid-hold instant, not the
* post-release state (release/pop happen after this function
* returns, in the caller) -- see FABRIC-3.6.md task 3.8's own
* write-up. console_println, not log_message(): confirmed live
* (this repl.c's own log_message() calls, at every level, do not
* appear anywhere in a real serial-log boot capture in this build
* -- log_message()'s fprintf(stderr, ...) is not wired to the
* serial console here) -- log_message() would have been silently
* invisible, defeating the point of evidence. One line, not two
* (dropped the earlier pre-send line from KH-BLK-ATTACH-SEND
* below): this fires once per real USB attach, not per word, so
* it is not the console_println-overuse case memory
* project_production_logging_cleanup_needed flags. */
{
VMRegistryEntry artemis_entry;
uint64_t held, pulled, returned, consumed;
char line[160];
int conserved = -1; /* -1 = Artemis not found */
sk_hermes_ledger(&held, &pulled, &returned, &consumed);
if (capsule_vm_find_by_name_nocase("Artemis", &artemis_entry) == 0 &&
artemis_entry.vm_ptr) {
conserved = stadium_conserved(artemis_entry.vm_id);
}
snprintf(line, sizeof(line),
"Kernel-Hermes BLK-ATTACH-EVENT (real, Stage C): ledger held=%llu "
"pulled=%llu returned=%llu consumed=%llu stadium_conserved(Artemis)=%s",
(unsigned long long)held, (unsigned long long)pulled,
(unsigned long long)returned, (unsigned long long)consumed,
conserved < 0 ? "UNKNOWN" : (conserved ? "true" : "FALSE"));
console_println(line);
}
homeblocks_sig_result_t sig_rc = g_blk_attach_pending[found_slot].sig_rc;
homeblocks_sig_t sig = g_blk_attach_pending[found_slot].sig;
int is_artemis = g_blk_attach_pending[found_slot].is_artemis;
@@ -281,8 +323,88 @@ static void sk_word_blk_attach_ack(VM *vm) {
}
}
/* FABRIC-3.6.md task 3.8 (Stage C, SXXXIV.2/.3): BLK-ATTACH-EVENT's real
* cutover -- the reply leg (Artemis -> Hera ack) that used to flow
* through common:messaging.4th's MSG-SEND/MSG-TICK now goes through
* kernel-Hermes's sk_hermes_send_one()/sk_hermes_drain_checkpoint()
* (tasks 3.3/3.4/3.6) instead. FORTH Hermes never sees a BLK-ATTACH-
* EVENT message again -- exactly SXXXIV.2's partition rule ("one owner
* per message, never shared"). The request leg (Hera -> Artemis,
* kernel_main.c's own "S\" <dev-addr> HERA-BLK-ATTACH-REQ\" S\"
* Artemis\" VM-EXEC" a few lines up) was never real FORTH messaging
* traffic to begin with -- no type tag, no arena, a direct VM-EXEC
* forced by Hera's own STADIUM-* colon-word limitation documented
* above -- so it is untouched here; this task cuts over the one
* message type that actually flowed through a messaging layer.
*
* sk_hermes_drain_checkpoint() (task 3.4) calls vm_interpret() directly
* on a message's payload_addr, so it must be NUL-terminated -- Artemis's
* own ATTACH-ACK-BUF (artemis:init.4th) is a raw CMOVE'd byte buffer
* with no such guarantee, so this copies into its own NUL-terminated
* buffer rather than passing ATTACH-ACK-BUF's address through
* unchanged. Static, not stack-local: sk_hermes_send_one() stores
* payload_addr out-of-line and the caller must keep it alive until
* drained (kernel_hermes.h's own documented contract) -- same single-
* buffer-reuse shape ATTACH-ACK-BUF itself already had (a second attach
* before the first drains overwrites it), not a new hazard this
* introduces. */
#define SK_KH_BLK_ATTACH_BUF_SIZE 80
static char g_kh_blk_attach_buf[SK_KH_BLK_ATTACH_BUF_SIZE];
/* KH-BLK-ATTACH-SEND ( paddr plen -- ok? ): copies the caller's payload
* (Artemis's own ATTACH-ACK-BUF content) into the NUL-terminated buffer
* above and sends it to Hera via kernel-Hermes. `from`/`to` are derived
* from the calling VM and sk_get_mama_vm() -- the caller never has to
* name a VMUuid, matching how HERA-BLK-ATTACH-REQ never had to before
* either (FORTH's own IDX 0/1/2 convention did that translation for
* it). Refusal (reservoir/arena/destination-queue exhaustion) is logged
* rather than silently dropped -- FABRIC-3.5.md SXXXV.0 names silent
* failure as this project's single most common defect shape, and this
* is exactly the class of message (identity birth gates on it) where a
* silent drop would be expensive to ever notice. */
static void sk_word_kh_blk_attach_send(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "KH-BLK-ATTACH-SEND: stack underflow");
vm->error = 1;
return;
}
cell_t plen = vm_pop(vm);
cell_t paddr = vm_pop(vm);
/* paddr is a VM-relative offset (vaddr_t), not a host pointer --
* CLAUDE.md's own "Important Conventions": "Stack values are VM
* offsets (vaddr_t), not C pointers -- use VM_ADDR()/CELL()".
* mama_forth_words.c's own VM-EXEC/VM-CALL words all translate a
* popped paddr the same way (vm_ptr(vm, (vaddr_t)caddr)) before
* touching it as a C pointer -- found live, not assumed: the first
* cut here raw-cast paddr directly and silently read all-zero
* memory (some unmapped/zeroed low address), producing an empty
* NUL-terminated payload that vm_interpret() executed as a no-op --
* no crash, no error, just a message that arrived and did nothing.
* Diagnosed via a temporary probe printing the copied buffer content
* at send time (per feedback_revert_probes_after_capture); reverted
* once this fix confirmed correct. */
const char *src = (const char *)vm_ptr(vm, (vaddr_t)paddr);
size_t n = (size_t)plen;
VM *mama_vm = (VM *)sk_get_mama_vm();
size_t i;
int rc;
if (n >= SK_KH_BLK_ATTACH_BUF_SIZE) n = SK_KH_BLK_ATTACH_BUF_SIZE - 1;
for (i = 0; i < n; i++) g_kh_blk_attach_buf[i] = src[i];
g_kh_blk_attach_buf[n] = '\0';
rc = sk_hermes_send_one(vm->stadium_vm_id, mama_vm->stadium_vm_id,
SK_HERMES_MSG_TYPE_BLK_ATTACH, 0,
g_kh_blk_attach_buf, (uint32_t)(n + 1));
if (rc != 0) {
log_message(LOG_ERROR, "KH-BLK-ATTACH-SEND: kernel-Hermes refused the send");
}
vm_push(vm, rc == 0 ? 1 : 0);
}
void sk_repl_register_words(VM *vm) {
register_word(vm, "BLK-ATTACH-ACK", sk_word_blk_attach_ack);
register_word(vm, "KH-BLK-ATTACH-SEND", sk_word_kh_blk_attach_send);
}
/*===========================================================================