ACK/NACK and private-channel negotiation -- FABRIC-3.6.md task 3.6
Extracted sk_hermes_send_one() from sk_hermes_publish()'s own
per-subscriber body -- one code path for both point-to-point and
fan-out delivery, so the ledger can never diverge between them.
Point-to-point addressing turned out to be load-bearing, not
incidental: sk_hermes_publish()'s fan-out sets msg->to to whichever
member it is iterating, so a negotiation message "published" to the
common channel would spuriously reach every member, not just the real
target (checked with advisor() before building the naive version).
"Over the common channel" means every VM is reachable from birth (task
3.2), not that the exchange itself fans out -- messaging.4th's own
CH-REQUEST carried an explicit `to` for the same reason.
sk_hermes_channel_request/respond/close build the mechanics: request ->
grant (creates a private channel, subscribes both parties, sends
CH_GRANT + one ACK) or NACK ("a deny is a NACK", SXLV.1 -- no separate
type); close authorized by membership alone. The grant/deny decision is
a plain caller-supplied `approved` bool -- task 3.7 replaces the call
site that produces it with a real ACL.4th query, not this signature.
Self-test covers the task's own three checks plus a sibling advisor()
flagged: an approved respond() whose channel creation itself fails
(table exhausted) must still fall through to NACK, not a silent false
grant or half-open channel -- verified by exhausting the whole channel
table and confirming the fallback.
Bug found and fixed before this was called done: the first draft
dropped a message via pending_pop() alone, without releasing it first,
leaking its Stadium heat and failing the self-test's own ledger
baseline check (logs/20260922-065946/amd64/, kept as audit trail).
Fixed and re-verified PASS on all three architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8a2ee0fdad
commit
205a49ecd0
@@ -1620,6 +1620,135 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
}
|
||||
}
|
||||
|
||||
/* FABRIC-3.6.md task 3.6 self-test: ACK/NACK and private-channel
|
||||
* negotiation. Covers exactly the task's own check -- grant path,
|
||||
* deny path, close path, heat conserved across all three -- plus
|
||||
* the sibling case the check text doesn't name but advisor() flagged
|
||||
* as the one a green boot would hide: an "approved" respond() whose
|
||||
* channel creation itself fails (table exhausted) must still fall
|
||||
* through to NACK, not a silent false grant or a half-open channel.
|
||||
* The grant/deny DECISION is a plain caller-supplied bool here --
|
||||
* the real ACL.4th query is task 3.7's scope, not this one's. */
|
||||
{
|
||||
VMUuid requester_id, target_id;
|
||||
int grant_rc1, grant_rc2;
|
||||
int neg_ok = 1;
|
||||
int ch1 = -1, ch2;
|
||||
uint64_t held0, pulled0, returned0, consumed0;
|
||||
uint64_t held1, pulled1, returned1, consumed1;
|
||||
|
||||
requester_id.hi = 0; requester_id.lo = 15;
|
||||
target_id.hi = 0; target_id.lo = 16;
|
||||
|
||||
console_puts("Kernel-Hermes negotiation self-test: ");
|
||||
grant_rc1 = stadium_grant_quota(requester_id, vm_uuid_hera());
|
||||
grant_rc2 = stadium_grant_quota(target_id, vm_uuid_hera());
|
||||
if (grant_rc1 != 0 || grant_rc2 != 0) {
|
||||
console_println("SKIPPED (quota grant failed)");
|
||||
} else {
|
||||
sk_hermes_ledger(&held0, &pulled0, &returned0, &consumed0);
|
||||
|
||||
/* --- Grant path --- */
|
||||
if (neg_ok && sk_hermes_channel_request(requester_id, target_id) != 0) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_pending_count(target_id) != 1) neg_ok = 0;
|
||||
if (neg_ok) {
|
||||
SkHermesMessage *req = sk_hermes_pending_peek(target_id);
|
||||
if (!req || req->type != SK_HERMES_MSG_TYPE_CH_REQUEST ||
|
||||
!vm_uuid_equal(req->from, requester_id)) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_release(req) != 0) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_pending_pop(target_id) != 0) neg_ok = 0;
|
||||
}
|
||||
if (neg_ok) {
|
||||
ch1 = sk_hermes_channel_respond(target_id, requester_id, 1);
|
||||
if (ch1 < 0) neg_ok = 0;
|
||||
}
|
||||
if (neg_ok && (!sk_hermes_channel_is_member(ch1, requester_id) ||
|
||||
!sk_hermes_channel_is_member(ch1, target_id))) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_pending_count(requester_id) != 2) neg_ok = 0; /* GRANT + ACK */
|
||||
if (neg_ok) {
|
||||
SkHermesMessage *m1 = sk_hermes_pending_peek(requester_id);
|
||||
if (!m1 || m1->type != SK_HERMES_MSG_TYPE_CH_GRANT ||
|
||||
m1->channel != (uint32_t)ch1) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_release(m1) != 0) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_pending_pop(requester_id) != 0) neg_ok = 0;
|
||||
}
|
||||
if (neg_ok) {
|
||||
SkHermesMessage *m2 = sk_hermes_pending_peek(requester_id);
|
||||
if (!m2 || m2->type != SK_HERMES_MSG_TYPE_ACK) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_release(m2) != 0) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_pending_pop(requester_id) != 0) neg_ok = 0;
|
||||
}
|
||||
if (neg_ok && !stadium_conserved(requester_id)) neg_ok = 0;
|
||||
if (neg_ok && !stadium_conserved(target_id)) neg_ok = 0;
|
||||
|
||||
/* --- Close path, on the channel just granted --- */
|
||||
if (neg_ok && sk_hermes_channel_close(requester_id, ch1) != 0) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_channel_is_member(ch1, target_id)) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_channel_destroy(ch1) == 0) neg_ok = 0; /* already gone */
|
||||
|
||||
/* --- Deny path --- */
|
||||
if (neg_ok && sk_hermes_channel_request(requester_id, target_id) != 0) neg_ok = 0;
|
||||
if (neg_ok) {
|
||||
/* Drop the request copy -- release its heat before
|
||||
* popping, same as every other drain in this self-test.
|
||||
* A bare pending_pop() alone leaks the message's Stadium
|
||||
* heat (it only advances the queue, per its own doc
|
||||
* comment -- caught live: this exact omission failed the
|
||||
* self-test's own held0/held1 baseline check). */
|
||||
SkHermesMessage *dropped = sk_hermes_pending_peek(target_id);
|
||||
if (!dropped) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_release(dropped) != 0) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_pending_pop(target_id) != 0) neg_ok = 0;
|
||||
}
|
||||
ch2 = -999;
|
||||
if (neg_ok) {
|
||||
ch2 = sk_hermes_channel_respond(target_id, requester_id, 0);
|
||||
if (ch2 != -1) neg_ok = 0;
|
||||
}
|
||||
if (neg_ok && sk_hermes_pending_count(requester_id) != 1) neg_ok = 0; /* NACK only */
|
||||
if (neg_ok) {
|
||||
SkHermesMessage *m3 = sk_hermes_pending_peek(requester_id);
|
||||
if (!m3 || m3->type != SK_HERMES_MSG_TYPE_NACK) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_release(m3) != 0) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_pending_pop(requester_id) != 0) neg_ok = 0;
|
||||
}
|
||||
|
||||
/* --- Grant-attempt-fails path: exhaust the channel table,
|
||||
* then confirm approved==1 still falls through to NACK
|
||||
* cleanly -- no half-open channel, no silent false grant.
|
||||
* The table is otherwise empty (indices 1..cap-1 free) at
|
||||
* this point, so exhausting and then destroying 1..cap-1
|
||||
* restores it exactly. */
|
||||
if (neg_ok) {
|
||||
int cap = sk_hermes_channel_capacity();
|
||||
int i;
|
||||
int ch3;
|
||||
|
||||
while (sk_hermes_channel_create() >= 0) { /* fill the table */ }
|
||||
|
||||
ch3 = sk_hermes_channel_respond(target_id, requester_id, 1);
|
||||
if (ch3 != -1) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_pending_count(requester_id) != 1) neg_ok = 0; /* NACK only */
|
||||
if (neg_ok) {
|
||||
SkHermesMessage *m4 = sk_hermes_pending_peek(requester_id);
|
||||
if (!m4 || m4->type != SK_HERMES_MSG_TYPE_NACK) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_release(m4) != 0) neg_ok = 0;
|
||||
if (neg_ok && sk_hermes_pending_pop(requester_id) != 0) neg_ok = 0;
|
||||
}
|
||||
|
||||
for (i = 1; i < cap; i++) sk_hermes_channel_destroy(i);
|
||||
}
|
||||
|
||||
sk_hermes_ledger(&held1, &pulled1, &returned1, &consumed1);
|
||||
if (held1 != held0) neg_ok = 0; /* every allocation released, back to baseline */
|
||||
if (!sk_hermes_audit_values(held1, pulled1, returned1, consumed1)) neg_ok = 0;
|
||||
if (!stadium_conserved(requester_id)) neg_ok = 0;
|
||||
if (!stadium_conserved(target_id)) neg_ok = 0;
|
||||
|
||||
console_println(neg_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
|
||||
|
||||
@@ -481,6 +481,42 @@ static int queue_push(SkHermesPendingQueue *q, int msg_index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_id,
|
||||
void *payload_addr, uint32_t payload_len) {
|
||||
SkHermesMessage *msg;
|
||||
SkHermesPendingQueue *q;
|
||||
int idx;
|
||||
|
||||
/* 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;
|
||||
|
||||
if (sk_hermes_alloc(from, &msg) != 0) return -1;
|
||||
|
||||
msg->type = type;
|
||||
msg->from = from;
|
||||
msg->to = to;
|
||||
msg->payload_addr = payload_addr;
|
||||
msg->payload_len = payload_len;
|
||||
msg->channel = channel_id;
|
||||
|
||||
q = find_or_create_queue(to);
|
||||
if (!q) {
|
||||
sk_hermes_release(msg); /* roll back -- no destination queue */
|
||||
return -1;
|
||||
}
|
||||
|
||||
idx = (int)(msg - sk_hermes_msgs);
|
||||
if (queue_push(q, idx) != 0) {
|
||||
sk_hermes_release(msg); /* roll back -- destination queue full */
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sk_hermes_publish(VMUuid from, int channel_id, uint32_t type,
|
||||
void *payload_addr, uint32_t payload_len) {
|
||||
SkHermesMembership *m;
|
||||
@@ -488,41 +524,18 @@ 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;
|
||||
if (payload_len > SK_HERMES_CHUNK_MAX_PAYLOAD) return -1; /* see sk_hermes_send_one()'s
|
||||
* own check -- duplicated here
|
||||
* so publish() itself returns
|
||||
* -1 (not a silent 0) on an
|
||||
* oversized payload */
|
||||
|
||||
m = &sk_hermes_channels[channel_id].membership;
|
||||
for (i = 0; i < m->count; i++) {
|
||||
VMUuid to = m->members[i];
|
||||
SkHermesMessage *msg;
|
||||
SkHermesPendingQueue *q;
|
||||
int idx;
|
||||
|
||||
if (sk_hermes_alloc(from, &msg) != 0) continue; /* this subscriber skipped, not fatal */
|
||||
|
||||
msg->type = type;
|
||||
msg->from = from;
|
||||
msg->to = to;
|
||||
msg->payload_addr = payload_addr;
|
||||
msg->payload_len = payload_len;
|
||||
msg->channel = (uint32_t)channel_id;
|
||||
|
||||
q = find_or_create_queue(to);
|
||||
if (!q) {
|
||||
sk_hermes_release(msg); /* roll back this subscriber's allocation */
|
||||
continue;
|
||||
if (sk_hermes_send_one(from, m->members[i], type, (uint32_t)channel_id,
|
||||
payload_addr, payload_len) == 0) {
|
||||
delivered++;
|
||||
}
|
||||
|
||||
idx = (int)(msg - sk_hermes_msgs);
|
||||
if (queue_push(q, idx) != 0) {
|
||||
sk_hermes_release(msg); /* this subscriber's own queue is full */
|
||||
continue;
|
||||
}
|
||||
|
||||
delivered++;
|
||||
}
|
||||
return delivered;
|
||||
}
|
||||
@@ -659,4 +672,45 @@ int sk_hermes_reassemble(SkHermesMessage **chunks, int n_chunks,
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sk_hermes_channel_request(VMUuid requester, VMUuid target) {
|
||||
return sk_hermes_send_one(requester, target, SK_HERMES_MSG_TYPE_CH_REQUEST,
|
||||
SK_HERMES_CHANNEL_COMMON, (void *)0, 0);
|
||||
}
|
||||
|
||||
int sk_hermes_channel_respond(VMUuid target, VMUuid requester, int approved) {
|
||||
int ch = -1;
|
||||
|
||||
if (approved) {
|
||||
ch = sk_hermes_channel_create();
|
||||
if (ch >= 0) {
|
||||
if (sk_hermes_channel_subscribe(ch, requester) != 0 ||
|
||||
sk_hermes_channel_subscribe(ch, target) != 0) {
|
||||
sk_hermes_channel_destroy(ch); /* leave no half-open channel */
|
||||
ch = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ch >= 0) {
|
||||
sk_hermes_send_one(target, requester, SK_HERMES_MSG_TYPE_CH_GRANT,
|
||||
(uint32_t)ch, (void *)0, 0);
|
||||
/* Ruled ACK cadence (2026-09-21): channel-open + delivery, not
|
||||
* every message -- this IS that moment. */
|
||||
sk_hermes_send_one(target, requester, SK_HERMES_MSG_TYPE_ACK,
|
||||
(uint32_t)ch, (void *)0, 0);
|
||||
} else {
|
||||
/* SXLV.1: "a deny is a NACK" -- no separate CH_DENY type. */
|
||||
sk_hermes_send_one(target, requester, SK_HERMES_MSG_TYPE_NACK,
|
||||
SK_HERMES_CHANNEL_COMMON, (void *)0, 0);
|
||||
}
|
||||
|
||||
return ch;
|
||||
}
|
||||
|
||||
int sk_hermes_channel_close(VMUuid closer, int channel_id) {
|
||||
if (channel_id == SK_HERMES_CHANNEL_COMMON) return -1;
|
||||
if (!sk_hermes_channel_is_member(channel_id, closer)) return -1;
|
||||
return sk_hermes_channel_destroy(channel_id);
|
||||
}
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
|
||||
Reference in New Issue
Block a user