Payload bound and chunking -- FABRIC-3.6.md task 3.5

sk_hermes_publish() now enforces SK_HERMES_CHUNK_MAX_PAYLOAD (1024) on
every message's payload_len uniformly, chunked or not -- closing the
gap task 3.3 explicitly parked. A chunk carrier is
[SkHermesChunkHeader][content slice], slice capped at
1024 - sizeof(header) rather than 1024 itself, so every message on the
wire satisfies the same one-block bound vm_interpret()'s own drain
limit already requires -- a future chunk-aware drain never has to
special-case a carrier that can't be handed to vm_interpret() as-is.

Deliberately no chunking-sender API: building one would need
kernel-Hermes to own chunk-buffer memory with a real lifetime it has no
way to track (kept alive until every subscriber drains it). Sending is
a loop pattern a caller writes with sk_hermes_chunk_count() +
sk_hermes_publish(), demonstrated by this task's own self-test.
sk_hermes_reassemble() is pure and memory-agnostic: validates msg_id
agreement, exact seq coverage, and per-chunk slice sizes before a
single memcpy, with the total length computed once and checked against
the caller's buffer once -- never order-dependent on which chunk
happens to overflow.

Verified live on all three architectures: a 1024-byte payload as one
message, a 1025-byte send refused outright with the ledger untouched,
and a 3000-byte payload split into 3 chunks, drained, and reassembled
byte-exact against the original. dict_hash unmoved and identical
across architectures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-22 06:51:48 -04:00
co-authored by Claude Sonnet 5
parent 2b1ba031a5
commit 8a2ee0fdad
7 changed files with 28126 additions and 6 deletions
+116
View File
@@ -1504,6 +1504,122 @@ static void kernel_main_deep(BootInfo *boot_info) {
}
}
/* FABRIC-3.6.md task 3.5 self-test: payload bound and chunking.
* Three checks, exactly the task's own: a 1024-byte payload as one
* message; a 3000-byte payload chunked (sk_hermes_chunk_count()) and
* reassembled byte-exact (sk_hermes_reassemble()); a 1025-byte
* single-message send refused by sk_hermes_publish() itself. No
* chunking-SENDER API exists (deliberately, see kernel_hermes.h's
* own doc comment on this section) -- this self-test builds its own
* chunk buffers directly, the pattern a real caller would follow.
* Large working buffers are function-static, not stack locals, to
* stay clear of any kernel-stack-size assumption. */
{
static uint8_t chunk_src[3000];
static uint8_t chunk_buf[3][SK_HERMES_CHUNK_MAX_PAYLOAD];
static uint8_t chunk_out[3072];
static uint8_t oversize_payload[SK_HERMES_CHUNK_MAX_PAYLOAD + 1];
VMUuid pub_id4, sub_id4;
int ch;
int chunk_ok = 1;
int grant_rc;
uint32_t n_chunks, i;
uint64_t held_before, pulled_before, returned_before, consumed_before;
uint64_t held_after, pulled_after, returned_after, consumed_after;
pub_id4.hi = 0; pub_id4.lo = 13;
sub_id4.hi = 0; sub_id4.lo = 14;
console_puts("Kernel-Hermes chunk self-test: ");
grant_rc = stadium_grant_quota(pub_id4, vm_uuid_hera());
if (grant_rc != 0) {
console_println("SKIPPED (quota grant failed)");
} else {
ch = sk_hermes_channel_create();
if (ch < 0) chunk_ok = 0;
if (chunk_ok && sk_hermes_channel_subscribe(ch, sub_id4) != 0) chunk_ok = 0;
sk_hermes_ledger(&held_before, &pulled_before, &returned_before, &consumed_before);
/* Check 1: exactly SK_HERMES_CHUNK_MAX_PAYLOAD bytes -- one
* message, no chunk header, must be accepted. */
if (chunk_ok) {
static uint8_t one_block[SK_HERMES_CHUNK_MAX_PAYLOAD];
for (i = 0; i < SK_HERMES_CHUNK_MAX_PAYLOAD; i++) one_block[i] = (uint8_t)i;
if (sk_hermes_publish(pub_id4, ch, 0, one_block, SK_HERMES_CHUNK_MAX_PAYLOAD) != 1)
chunk_ok = 0;
if (chunk_ok && sk_hermes_pending_count(sub_id4) != 1) chunk_ok = 0;
if (chunk_ok) {
SkHermesMessage *msg = sk_hermes_pending_peek(sub_id4);
if (!msg || msg->payload_len != SK_HERMES_CHUNK_MAX_PAYLOAD) chunk_ok = 0;
if (chunk_ok && sk_hermes_release(msg) != 0) chunk_ok = 0;
if (chunk_ok && sk_hermes_pending_pop(sub_id4) != 0) chunk_ok = 0;
}
}
/* Check 3: one byte over the bound -- must be refused
* outright, no allocation, ledger untouched. */
if (chunk_ok) {
if (sk_hermes_publish(pub_id4, ch, 0, oversize_payload,
SK_HERMES_CHUNK_MAX_PAYLOAD + 1) != -1) chunk_ok = 0;
if (chunk_ok && sk_hermes_pending_count(sub_id4) != 0) chunk_ok = 0;
}
/* Check 2: 3000 bytes, chunked and reassembled byte-exact. */
if (chunk_ok) {
for (i = 0; i < sizeof(chunk_src); i++) chunk_src[i] = (uint8_t)((i * 7 + 3) & 0xFF);
n_chunks = sk_hermes_chunk_count(sizeof(chunk_src));
if (n_chunks == 0 || n_chunks > 3) chunk_ok = 0;
}
if (chunk_ok) {
uint32_t sent = 0;
for (i = 0; i < n_chunks; i++) {
SkHermesChunkHeader *h = (SkHermesChunkHeader *)&chunk_buf[i][0];
uint32_t remaining = (uint32_t)sizeof(chunk_src) - sent;
uint32_t slice = (remaining > SK_HERMES_CHUNK_MAX_SLICE) ?
SK_HERMES_CHUNK_MAX_SLICE : remaining;
h->msg_id = 0xC5;
h->seq = i;
h->is_last = (i == n_chunks - 1) ? 1 : 0;
memcpy(&chunk_buf[i][0] + sizeof(SkHermesChunkHeader), &chunk_src[sent], slice);
if (sk_hermes_publish(pub_id4, ch, 0, &chunk_buf[i][0],
(uint32_t)sizeof(SkHermesChunkHeader) + slice) != 1) chunk_ok = 0;
sent += slice;
}
if (chunk_ok && sent != sizeof(chunk_src)) chunk_ok = 0;
if (chunk_ok && sk_hermes_pending_count(sub_id4) != (int)n_chunks) chunk_ok = 0;
}
if (chunk_ok) {
SkHermesMessage *msgs[3];
uint32_t out_len = 0;
for (i = 0; i < n_chunks; i++) {
msgs[i] = sk_hermes_pending_peek(sub_id4);
if (!msgs[i]) { chunk_ok = 0; break; }
if (sk_hermes_pending_pop(sub_id4) != 0) { chunk_ok = 0; break; }
}
if (chunk_ok &&
sk_hermes_reassemble(msgs, (int)n_chunks, chunk_out, sizeof(chunk_out), &out_len) != 0)
chunk_ok = 0;
if (chunk_ok && out_len != sizeof(chunk_src)) chunk_ok = 0;
if (chunk_ok && memcmp(chunk_out, chunk_src, sizeof(chunk_src)) != 0) chunk_ok = 0;
for (i = 0; i < n_chunks; i++) {
if (sk_hermes_release(msgs[i]) != 0) chunk_ok = 0;
}
}
sk_hermes_ledger(&held_after, &pulled_after, &returned_after, &consumed_after);
if (held_after != held_before) chunk_ok = 0; /* every allocation released, back to baseline */
if (!sk_hermes_audit_values(held_after, pulled_after, returned_after, consumed_after)) chunk_ok = 0;
if (!stadium_conserved(pub_id4)) chunk_ok = 0;
if (chunk_ok && sk_hermes_channel_destroy(ch) != 0) chunk_ok = 0;
console_println(chunk_ok ? "PASS" : "FAIL");
}
}
/* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present -- headless by default (EMERGENCY_CONSOLE_
* ENABLED off), reusing that flag's own existing "does this build
+72
View File
@@ -488,6 +488,11 @@ int sk_hermes_publish(VMUuid from, int channel_id, uint32_t type,
int delivered = 0;
if (!channel_valid(channel_id)) return -1;
/* Task 3.5: the one-block bound, enforced uniformly here whether or
* not payload_addr happens to be a chunk carrier -- see
* kernel_hermes.h's own sizing note on why SK_HERMES_CHUNK_MAX_SLICE
* (not SK_HERMES_CHUNK_MAX_PAYLOAD) is a chunk's own content size. */
if (payload_len > SK_HERMES_CHUNK_MAX_PAYLOAD) return -1;
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
@@ -587,4 +592,71 @@ int sk_hermes_drain_checkpoint(VM *vm) {
return drain_error ? -1 : 1;
}
uint32_t sk_hermes_chunk_count(uint32_t len) {
if (len == 0) return 0;
return (len + SK_HERMES_CHUNK_MAX_SLICE - 1) / SK_HERMES_CHUNK_MAX_SLICE;
}
int sk_hermes_reassemble(SkHermesMessage **chunks, int n_chunks,
uint8_t *out_buf, uint32_t out_buf_cap,
uint32_t *out_len) {
int present[SK_HERMES_MSG_MAX];
int i;
uint32_t msg_id;
uint32_t total;
uint32_t last_slice_len = 0;
if (!chunks || n_chunks <= 0 || !out_buf || !out_len) return -1;
if (n_chunks > SK_HERMES_MSG_MAX) return -1; /* guard before indexing present[] */
for (i = 0; i < n_chunks; i++) present[i] = 0;
if (!chunks[0] || !chunks[0]->payload_addr ||
chunks[0]->payload_len < (uint32_t)sizeof(SkHermesChunkHeader)) return -1;
msg_id = ((SkHermesChunkHeader *)chunks[0]->payload_addr)->msg_id;
for (i = 0; i < n_chunks; i++) {
SkHermesMessage *m = chunks[i];
SkHermesChunkHeader *h;
uint32_t slice_len;
if (!m || !m->payload_addr || m->payload_len < (uint32_t)sizeof(SkHermesChunkHeader)) return -1;
h = (SkHermesChunkHeader *)m->payload_addr;
slice_len = m->payload_len - (uint32_t)sizeof(SkHermesChunkHeader);
if (h->msg_id != msg_id) return -1;
if (h->seq >= (uint32_t)n_chunks) return -1;
if (present[h->seq]) return -1; /* duplicate seq */
present[h->seq] = 1;
if (h->seq == (uint32_t)(n_chunks - 1)) {
if (!h->is_last) return -1;
if (slice_len == 0 || slice_len > SK_HERMES_CHUNK_MAX_SLICE) return -1;
last_slice_len = slice_len;
} else {
if (h->is_last) return -1;
if (slice_len != SK_HERMES_CHUNK_MAX_SLICE) return -1;
}
}
for (i = 0; i < n_chunks; i++) if (!present[i]) return -1; /* gap */
/* Total computed once from validated seq completeness, checked once
* against out_buf_cap, before any memcpy -- never order-dependent on
* which chunk happens to overflow first. */
total = (uint32_t)(n_chunks - 1) * SK_HERMES_CHUNK_MAX_SLICE + last_slice_len;
if (total > out_buf_cap) return -1;
for (i = 0; i < n_chunks; i++) {
SkHermesMessage *m = chunks[i];
SkHermesChunkHeader *h = (SkHermesChunkHeader *)m->payload_addr;
uint32_t slice_len = m->payload_len - (uint32_t)sizeof(SkHermesChunkHeader);
uint32_t offset = h->seq * SK_HERMES_CHUNK_MAX_SLICE;
memcpy(out_buf + offset, (uint8_t *)m->payload_addr + sizeof(SkHermesChunkHeader), slice_len);
}
*out_len = total;
return 0;
}
#endif /* __STARKERNEL__ */