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
+104 -5
View File
@@ -416,12 +416,15 @@ int sk_hermes_queues_boot_init(void);
* (-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 (bound/chunking is task 3.5's scope, not
* this one's -- 3.3 does not enforce a payload size
* limit).
* @param payload_len Payload length in bytes, passed through unchanged.
* unchanged.
* @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
* since task 3.3 deliberately parked this check --
* see that section's own doc comment below).
* @return Count of subscribers successfully enqueued to (0..member count),
* or -1 if channel_id itself was invalid.
* or -1 if channel_id itself was invalid or payload_len exceeded
* the bound.
*/
int sk_hermes_publish(VMUuid from, int channel_id, uint32_t type,
void *payload_addr, uint32_t payload_len);
@@ -510,6 +513,102 @@ typedef struct VM VM;
*/
int sk_hermes_drain_checkpoint(VM *vm);
/*
* Payload bound and chunking (FABRIC-3.6.md task 3.5, B4 / FABRIC-3.5.md
* SXLV.3; chunk framing ruled 2026-09-21: each chunk carries (msg_id,
* seq, is_last) ahead of the real content).
*
* Sizing decision (not itself ruled -- see SXLV.4's own note that the
* concrete framing was a task-writing prerequisite, not a ruling):
* SK_HERMES_CHUNK_MAX_PAYLOAD (1024, one block, SXLIV.1) bounds a
* message's payload_len UNIFORMLY -- chunked or not. A chunk carrier's
* own payload is [SkHermesChunkHeader][content slice], so the slice
* itself is capped at 1024 - sizeof(SkHermesChunkHeader), not 1024,
* keeping every message on the wire under the same one-block bound
* SXLIII.6.1 already requires for vm_interpret()'s own drain limit.
* (The alternative -- slice up to 1024, carrier up to
* 1024+sizeof(header) -- was considered and rejected: it would mean a
* chunk carrier can never be handed to vm_interpret() as-is, making a
* later chunk-aware drain a special case instead of the same drain path
* every other message already uses.)
*
* sk_hermes_publish() (task 3.3, above) enforces this bound directly --
* refuses (-1) any single payload_len over SK_HERMES_CHUNK_MAX_PAYLOAD,
* whether or not it's a chunk carrier, since both cases hold the same
* invariant now.
*
* SENDING a payload over SK_HERMES_CHUNK_MAX_SLICE bytes is deliberately
* NOT a kernel-Hermes API here -- it is a loop a caller writes with the
* primitives below plus sk_hermes_publish(), the same way this task's
* own self-test proves reassembly (kernel_main.c: build N static chunk
* buffers, sk_hermes_publish() each). Building a chunking sender would
* need kernel-Hermes to own chunk-buffer memory with a real lifetime
* (kept alive until every subscriber has drained it, freed only once
* kernel-Hermes has no way to know that) -- a genuine new question this
* task's inert scope does not call for. A real multi-chunk sender is a
* later task's problem, when a real message type actually needs one
* (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
/* SkHermesChunkHeader - prepended to a chunk carrier's own payload,
* ahead of up to SK_HERMES_CHUNK_MAX_SLICE bytes of real content.
* msg_id is caller-chosen and must be unique per logical multi-chunk
* send from a given sender -- reassembly groups chunks by it. seq is
* 0-based, ascending, no gaps, {0..n_chunks-1}. is_last is set on
* exactly the chunk whose seq is n_chunks-1, no other. */
typedef struct {
uint32_t msg_id;
uint32_t seq;
int is_last;
} SkHermesChunkHeader;
/* Content bytes per chunk -- the one-block bound minus header overhead. */
#define SK_HERMES_CHUNK_MAX_SLICE (SK_HERMES_CHUNK_MAX_PAYLOAD - (uint32_t)sizeof(SkHermesChunkHeader))
/* sk_hermes_chunk_count - number of SK_HERMES_CHUNK_MAX_SLICE-sized
* chunks `len` bytes needs (ceiling division). 0 bytes needs 0 chunks;
* 1..SK_HERMES_CHUNK_MAX_SLICE needs 1; and so on. Pure arithmetic, no
* side effects -- a caller sizing its own chunk-send loop calls this
* first. */
uint32_t sk_hermes_chunk_count(uint32_t len);
/* sk_hermes_reassemble - concatenates n_chunks chunk carrier messages
* (already drained off a subscriber's own pending queue via
* sk_hermes_pending_peek()/sk_hermes_pending_pop(), task 3.3) sharing
* one msg_id back into one byte-exact buffer at out_buf, in ascending
* seq order regardless of the order chunks[] itself is passed in.
*
* Refuses (-1, *out_len untouched, no partial copy ever lands in
* out_buf) if: n_chunks is 0 or exceeds SK_HERMES_MSG_MAX (the system-
* wide message arena size -- chunking can never need more chunk-carrier
* messages than the whole arena holds); any chunk is NULL, has no
* payload, or its payload is shorter than sizeof(SkHermesChunkHeader);
* the chunks' msg_ids disagree; seq values are not exactly
* {0..n_chunks-1} (a duplicate or a gap); is_last is not set on exactly
* the seq==n_chunks-1 chunk and no other; a non-final chunk's content
* slice is not exactly SK_HERMES_CHUNK_MAX_SLICE bytes, or the final
* chunk's is 0 or over that bound; or the total reassembled length
* would exceed out_buf_cap (checked once, from validated seq
* completeness, before any memcpy -- never order-dependent on which
* chunk happens to overflow first).
*
* @param chunks Array of n_chunks chunk carrier message pointers.
* @param n_chunks Chunk count (from sk_hermes_chunk_count() at send
* time, or simply len(chunks[])).
* @param out_buf Caller-owned destination buffer.
* @param out_buf_cap Capacity of out_buf in bytes.
* @param out_len Set to the reassembled length on success only.
* @return 0 on success, -1 on any refusal above.
*/
int sk_hermes_reassemble(SkHermesMessage **chunks, int n_chunks,
uint8_t *out_buf, uint32_t out_buf_cap,
uint32_t *out_len);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_KERNEL_HERMES_H */