Fix SkHermesMessage payload-aliasing defect (task 3.8 findings log)

sk_hermes_send_one() -- the single funnel every sender, including
sk_hermes_publish(), already goes through -- used to store the
caller's own payload_addr pointer as-is. Two sends before either
drains meant both messages pointed at the same caller-owned buffer,
whichever send wrote last silently winning: real, confirmed live (a
second identity thumbdrive attached at boot alongside Zuse's own left
its WIREBIND pairing silently never happening).

SkHermesMessage gains an inline payload_buf[SK_HERMES_CHUNK_MAX_
PAYLOAD] field; sk_hermes_send_one() now memcpy()s the caller's
payload into it and points payload_addr at that copy instead. No
sender or reader call site needed to change -- every existing reader
already only ever reads through payload_addr, which still points at
valid bytes of the same length, now message-owned. g_kh_blk_attach_buf/
g_kh_console_cmd_buf (repl.c, tasks 3.8/3.9) no longer need to survive
past their own send call; their doc comments, which had claimed the
old aliasing shape was benign, are corrected.

Verified the original bug is actually gone: reproduced the exact
original scenario (a real, sequentially-minted rajames identity
attached at boot alongside Zuse's own, two simultaneous BLK-ATTACH-
EVENT sends in one idle-loop pass) -- WIREBIND pairing, USE, and the
Stage D relay all now work where WIREBIND previously silently failed.
Standard three-ISA acceptance also clean: zero UNKNOWN WORD, dict_hash
identical across all three and matching every prior acceptance run in
this document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-22 14:16:29 -04:00
co-authored by Claude Sonnet 5
parent 21734b1a52
commit 5c07745c18
14 changed files with 46639 additions and 35 deletions
+48 -9
View File
@@ -73,6 +73,13 @@
#define SK_HERMES_MSG_MAX 32
#define SK_HERMES_MEMBER_MAX 64
/* SK_HERMES_CHUNK_MAX_PAYLOAD - one block (1024 bytes, 64x16, SXLIV.1),
* moved up here (was originally defined much further down, next to
* SkHermesChunkHeader) so SkHermesMessage's own payload_buf field below
* can be sized against it. See this file's chunking section, further
* down, for the full sizing rationale -- not restated here. */
#define SK_HERMES_CHUNK_MAX_PAYLOAD 1024
/*
* SK_HERMES_Q_SLOT - per-message admission heat, task 2.2 (item 28).
* messaging.4th:44-46 derives Q.SLOT as the reservoir remaining after
@@ -98,11 +105,35 @@
* reserved sentinels MSG-NACKED(253)/MSG-DELIVERED(255)).
* @field from Sending VM (mirrors MSG-FROM@).
* @field to Target VM (mirrors MSG-TO@).
* @field payload_addr Out-of-line payload address (mirrors MSG-PADDR@).
* @field payload_addr Payload address a reader dereferences (mirrors
* MSG-PADDR@). CORRECTED 2026-09-22 (real defect,
* FABRIC-3.6.md task 3.8's own findings log, "a real
* payload-aliasing defect"): this used to be the
* caller's own out-of-line pointer, stored as-is --
* two sends before either drained meant both
* messages pointed at the same caller-owned buffer,
* whichever send wrote last silently winning. Now
* always points at this SAME message's own
* `payload_buf` below, which sk_hermes_send_one()
* copies into at send time -- every message owns its
* payload bytes, independent of whatever the caller
* does with its own buffer afterward. Every existing
* reader (sk_hermes_drain_checkpoint(),
* sk_hermes_reassemble()) is unaffected: `payload_addr`
* still points at valid payload bytes of the same
* length, just message-owned instead of caller-owned.
* FABRIC-3.5.md SXLIII.6.1 (item 44, still open): a
* payload above INPUT_BUFFER_SIZE-1 (1024) bytes cannot
* be drained in one interpret call -- bound or chunk it
* before Phase 3, not here.
* @field payload_buf Backing storage for `payload_addr` above -- see its
* own doc comment. Sized to SK_HERMES_CHUNK_MAX_PAYLOAD,
* the uniform bound sk_hermes_send_one()/
* sk_hermes_publish() already enforce on `payload_len`
* (defined just above, moved up in this same fix so it
* is in scope here). No reader should touch this field
* directly -- go through `payload_addr`, which is what
* every existing reader already does.
* @field payload_len Payload length in bytes (mirrors MSG-PLEN@).
* @field stadium_cell Index into the Stadium cell array this message's
* heat currently occupies, or a sentinel meaning "none"
@@ -126,6 +157,7 @@ typedef struct {
VMUuid from;
VMUuid to;
void *payload_addr;
uint8_t payload_buf[SK_HERMES_CHUNK_MAX_PAYLOAD];
uint32_t payload_len;
int32_t stadium_cell;
uint32_t seq;
@@ -411,8 +443,12 @@ int sk_hermes_queues_boot_init(void);
* response encode context (e.g. task 3.6's GRANT
* uses this to tell the requester which private
* channel was just created).
* @param payload_addr Out-of-line payload address, passed through
* unchanged.
* @param payload_addr Caller-owned buffer, read and copied into the
* message's own storage before this function
* returns (2026-09-22 fix, this struct's own
* payload_addr field doc comment has the full
* account) -- the caller does not need to keep
* it alive past this call.
* @param payload_len Refused (-1) if it exceeds
* SK_HERMES_CHUNK_MAX_PAYLOAD.
* @return 0 on success, -1 on any refusal (bound, reservoir, arena, or
@@ -450,8 +486,10 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
* channel from sk_hermes_channel_create()). Refused
* (-1) if invalid/not in use.
* @param type Message type code, passed through unchanged.
* @param payload_addr Out-of-line payload address, passed through
* unchanged.
* @param payload_addr Caller-owned buffer, read once per subscriber via
* sk_hermes_send_one() (its own doc comment has the
* full account) -- the caller does not need to keep
* it alive past this call.
* @param payload_len Payload length in bytes. Refused (-1, no
* allocation attempted) if it exceeds
* SK_HERMES_CHUNK_MAX_PAYLOAD (task 3.5, added here
@@ -585,10 +623,11 @@ int sk_hermes_drain_checkpoint(VM *vm);
* (3.8+, the same boundary chunk-aware drain is already deferred past).
*/
/* One block = 1024 bytes (64x16, SXLIV.1) -- the uniform bound every
* message's payload_len must satisfy, chunked or not (see this
* section's own sizing note above). */
#define SK_HERMES_CHUNK_MAX_PAYLOAD 1024
/* SK_HERMES_CHUNK_MAX_PAYLOAD (1024 bytes, 64x16, SXLIV.1) -- the
* uniform bound every message's payload_len must satisfy, chunked or
* not (see this section's own sizing note above) -- is defined earlier
* in this file now, next to SkHermesMessage's own payload_buf field,
* which needs it in scope. Not redefined here. */
/* SkHermesChunkHeader - prepended to a chunk carrier's own payload,
* ahead of up to SK_HERMES_CHUNK_MAX_SLICE bytes of real content.