Stage M-0: kernel-Hermes arena, pending queues and member lists grow on demand; Q_SLOT its own constant (FABRIC-3.9.0 §7.0, M-0/M-0b)

- The static sk_hermes_msgs[32] is replaced by a directory of fixed-size
  segments (SK_HERMES_ARENA_SEGMENT, growth granularity only). Messages
  never move -- callers, including the drain across vm_interpret_kind(),
  hold message pointers across later allocations, so copy-on-grow (as
  §7.0 first proposed) would dangle them. Each message carries its index;
  numbering unchanged.
- Pending-queue rings and channel member lists heap-grown (doubling);
  member lists freed on destroy; reassembly scratch sized per call.
  SK_HERMES_MSG_MAX / _PENDING_MAX / _MEMBER_MAX are gone.
- Q_SLOT = Q48_ONE / 32 as its own constant (M-0b), value unchanged.
- Every grow failure is printed, logged at LOG_ERROR, counted
  (sk_hermes_grow_failures()) and refused whole; a send whose queue can't
  grow releases its message.

Probe (reverted), identical on amd64/aarch64/riscv64: 70 messages in one
queue (arena 32 -> 96), sender refused by its reservoir, 72 popped intact
in order, 100 members; a live Hera drain on a full arena grew it mid-drain
and released/popped the right message; each grow path's injected failure
refused whole and counted. 3-ISA acceptance passes; boot parity, dict_hash
set and the live ledger line byte-identical to before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-24 17:52:43 -04:00
co-authored by Claude Opus 5.5
parent 316be9fc4b
commit d8358a3011
16 changed files with 55512 additions and 69 deletions
+14 -6
View File
@@ -883,9 +883,14 @@ static void kernel_main_deep(BootInfo *boot_info) {
uint64_t reservoir0 = stadium_reservoir_peek(alloc_test_id);
uint64_t expected_n = reservoir0 / SK_HERMES_Q_SLOT;
uint64_t got_n = 0;
SkHermesMessage *msgs[SK_HERMES_MSG_MAX];
/* Stage M-0: the arena has no fixed size any more, so this list
* is sized from the reservoir (expected_n) plus one, and every
* loop below is bounded by msgs_cap -- the refusal past
* expected_n must now come from the reservoir, not the arena. */
uint64_t msgs_cap = expected_n + 1;
SkHermesMessage **msgs = (SkHermesMessage **)kmalloc((size_t)msgs_cap * sizeof(SkHermesMessage *));
SkHermesMessage *msg;
int ok = 1;
int ok = msgs ? 1 : 0;
size_t i;
uint64_t held0, pulled0, returned0, consumed0;
uint64_t held1, pulled1, returned1, consumed1;
@@ -896,7 +901,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
* only thing that has ever called these functions. */
sk_hermes_ledger(&held0, &pulled0, &returned0, &consumed0);
while (got_n < SK_HERMES_MSG_MAX && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
while (msgs && got_n < msgs_cap && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
msgs[got_n] = msg;
got_n++;
}
@@ -958,7 +963,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
uint64_t held3, pulled3, returned3, consumed3;
uint64_t held4, pulled4, returned4, consumed4;
uint64_t n2 = 0;
while (n2 < SK_HERMES_MSG_MAX && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
while (msgs && n2 < msgs_cap && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
msgs[n2] = msg;
n2++;
}
@@ -998,7 +1003,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
uint64_t expected_b = stadium_reservoir_peek(alloc_test_id) / SK_HERMES_Q_SLOT;
int sb = 1;
if (!stadium_conserved(alloc_test_id)) sb = 0; /* before */
while (nb < SK_HERMES_MSG_MAX && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
while (msgs && nb < msgs_cap && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
msgs[nb++] = msg;
}
if (nb != expected_b || nb == 0) sb = 0;
@@ -1025,7 +1030,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
int sc = 1;
uint64_t k = 0;
if (!sk_hermes_scan_check() || sk_hermes_scan_held(&live) != 0 || live != 0) sc = 0;
while (k < 4 && sk_hermes_alloc(alloc_test_id, &msg) == 0) msgs[k++] = msg;
while (msgs && k < 4 && k < msgs_cap && sk_hermes_alloc(alloc_test_id, &msg) == 0) msgs[k++] = msg;
if (k != 4) sc = 0;
s1 = sk_hermes_scan_held(&live);
sk_hermes_ledger(&h, &p_, &r, &c);
@@ -1073,6 +1078,9 @@ static void kernel_main_deep(BootInfo *boot_info) {
print_uint(" pulled(final)=", pulled2);
print_uint(" returned(final)=", returned2);
print_uint(" consumed(final)=", consumed2);
print_uint(" arena_capacity=", (uint64_t)sk_hermes_arena_capacity());
print_uint(" grow_failures=", sk_hermes_grow_failures());
if (msgs) kfree(msgs);
}
}
+167 -39
View File
@@ -51,6 +51,7 @@
#include "starkernel/q48_16.h"
#include "starkernel/kmalloc.h"
#include "console.h"
#include "log.h" /* Stage M-0: grow failures are logged at LOG_ERROR */
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.8.md §3.13.3 -- wait stamps */
@@ -72,7 +73,60 @@ static void console_put_u64(uint64_t v) {
console_puts(&buf[i]);
}
static SkHermesMessage sk_hermes_msgs[SK_HERMES_MSG_MAX];
/* FABRIC-3.9.0.md §7.0 (Stage M-0): the message arena grows on demand (was
* a static sk_hermes_msgs[32]). A directory of SK_HERMES_ARENA_SEGMENT-sized
* segments: messages never move once allocated (see kernel_hermes.h), only
* the directory is allocate-copy-free. A message's identity is its arena
* index, lowest free first -- the same numbering as the fixed array. */
static SkHermesMessage **sk_hermes_segs = (SkHermesMessage **)0;
static size_t sk_hermes_nsegs = 0;
static size_t sk_hermes_segs_cap = 0;
static uint64_t sk_hermes_grow_fail = 0;
uint64_t sk_hermes_grow_failures(void) { return sk_hermes_grow_fail; }
size_t sk_hermes_arena_capacity(void) { return sk_hermes_nsegs * SK_HERMES_ARENA_SEGMENT; }
/* Every grow failure: logged (persisted), printed in full, counted. The
* caller then refuses whole. */
static void sk_hermes_grow_failed(const char *what) {
sk_hermes_grow_fail++;
console_puts("Kernel-Hermes: can't grow ");
console_println(what);
log_message(LOG_ERROR, "Kernel-Hermes: can't grow %s", what);
}
static SkHermesMessage *msg_at(size_t idx) {
return &sk_hermes_segs[idx / SK_HERMES_ARENA_SEGMENT][idx % SK_HERMES_ARENA_SEGMENT];
}
/* Add one segment (and grow the directory first if it's full). */
static int sk_hermes_arena_grow(void) {
SkHermesMessage *seg;
if (sk_hermes_nsegs == sk_hermes_segs_cap) {
size_t ncap = sk_hermes_segs_cap ? sk_hermes_segs_cap * 2 : 4;
SkHermesMessage **dir = (SkHermesMessage **)kmalloc(ncap * sizeof(SkHermesMessage *));
size_t i;
if (!dir) {
sk_hermes_grow_failed("message arena directory");
return -1;
}
for (i = 0; i < sk_hermes_nsegs; i++) dir[i] = sk_hermes_segs[i];
if (sk_hermes_segs) kfree(sk_hermes_segs);
sk_hermes_segs = dir;
sk_hermes_segs_cap = ncap;
}
seg = (SkHermesMessage *)kmalloc(SK_HERMES_ARENA_SEGMENT * sizeof(SkHermesMessage));
if (!seg) {
sk_hermes_grow_failed("message arena");
return -1;
}
memset(seg, 0, SK_HERMES_ARENA_SEGMENT * sizeof(SkHermesMessage));
sk_hermes_segs[sk_hermes_nsegs++] = seg;
return 0;
}
/* Task 2.4 (item 28), FABRIC-3.5.md SXXXVII.3/SXL.4's four counters.
* `consumed` is declared now but touched nowhere yet -- task 2.5 adds
@@ -113,11 +167,12 @@ uint64_t sk_hermes_audit_failure_count(void) {
uint64_t sk_hermes_scan_held(size_t *live_count) {
uint64_t sum = 0;
size_t n = 0;
int i;
size_t i, len = sk_hermes_arena_capacity();
for (i = 0; i < SK_HERMES_MSG_MAX; i++) {
if (!sk_hermes_msgs[i].in_use || sk_hermes_msgs[i].stadium_cell < 0) continue;
sum += stadium_cells()[sk_hermes_msgs[i].stadium_cell].header.heat;
for (i = 0; i < len; i++) {
SkHermesMessage *m = msg_at(i);
if (!m->in_use || m->stadium_cell < 0) continue;
sum += stadium_cells()[m->stadium_cell].header.heat;
n++;
}
if (live_count) *live_count = n;
@@ -128,16 +183,20 @@ int sk_hermes_scan_check(void) {
return sk_hermes_scan_held((size_t *)0) == sk_hermes_held;
}
static int sk_hermes_find_free_slot(void) {
int i;
for (i = 0; i < SK_HERMES_MSG_MAX; i++) {
if (!sk_hermes_msgs[i].in_use) return i;
/* Lowest free index; grows the arena by a segment when every slot is in
* use. -1 only if that grow fails (logged, counted). */
static long sk_hermes_find_free_slot(void) {
size_t i, len = sk_hermes_arena_capacity();
for (i = 0; i < len; i++) {
if (!msg_at(i)->in_use) return (long)i;
}
return -1;
if (sk_hermes_arena_grow() != 0) return -1;
return (long)len; /* first slot of the new segment */
}
int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
int slot;
long slot;
SkHermesMessage *m;
uint64_t pulled;
StadiumPatronHeader candidate;
size_t cell;
@@ -150,7 +209,7 @@ int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
if (stadium_reservoir_peek(vm_id) < SK_HERMES_Q_SLOT) return -1;
slot = sk_hermes_find_free_slot();
if (slot < 0) return -1; /* arena full, reservoir untouched */
if (slot < 0) return -1; /* arena couldn't grow (logged, counted), reservoir untouched */
pulled = stadium_reservoir_pull(vm_id, SK_HERMES_Q_SLOT);
if (pulled < SK_HERMES_Q_SLOT) {
@@ -174,7 +233,7 @@ int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
candidate.ttl = 0; /* decay/TTL is task 2.5's scope, not this one's */
candidate.link = 0;
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1; /* message body lives in sk_hermes_msgs[], not here */
candidate.mass = 1; /* message body lives in the arena, not here */
candidate.flags = 0;
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_DELIVER;
@@ -188,11 +247,13 @@ int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
return -1;
}
memset(&sk_hermes_msgs[slot], 0, sizeof(SkHermesMessage));
sk_hermes_msgs[slot].in_use = 1;
sk_hermes_msgs[slot].stadium_cell = (int32_t)cell;
sk_hermes_msgs[slot].owner = vm_id;
*out_msg = &sk_hermes_msgs[slot];
m = msg_at((size_t)slot);
memset(m, 0, sizeof(SkHermesMessage));
m->in_use = 1;
m->stadium_cell = (int32_t)cell;
m->owner = vm_id;
m->index = (uint32_t)slot;
*out_msg = m;
/* Task 2.4: the one site where held/pulled increment -- only reached
* once every refusal path above has already returned. */
@@ -336,6 +397,11 @@ int sk_hermes_channel_destroy(int channel_id) {
if (channel_id == SK_HERMES_CHANNEL_COMMON) return -1; /* permanent, SXLV.1 */
if (!channel_valid(channel_id)) return -1;
/* Stage M-0: the member list is heap-grown -- free it, or create()'s
* memset of this slot would leak it. */
if (sk_hermes_channels[channel_id].membership.members) {
kfree(sk_hermes_channels[channel_id].membership.members);
}
memset(&sk_hermes_channels[channel_id], 0, sizeof(SkHermesChannel));
return 0;
}
@@ -350,7 +416,20 @@ int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id) {
for (i = 0; i < m->count; i++) {
if (vm_uuid_equal(m->members[i], vm_id)) return -1; /* already a member */
}
if (m->count >= SK_HERMES_MEMBER_MAX) return -1;
if (m->count == m->cap) {
/* Stage M-0: grow the member list (was a fixed [64]) -- doubling,
* allocate-copy-free, all-or-nothing. */
size_t ncap = m->cap ? m->cap * 2 : 8;
VMUuid *grown = (VMUuid *)kmalloc(ncap * sizeof(VMUuid));
if (!grown) {
sk_hermes_grow_failed("channel member list");
return -1;
}
for (i = 0; i < m->count; i++) grown[i] = m->members[i];
if (m->members) kfree(m->members);
m->members = grown;
m->cap = ncap;
}
m->members[m->count] = vm_id;
m->count++;
@@ -396,17 +475,21 @@ int sk_hermes_channel_member_count(int channel_id) {
}
/* Task 3.3: per-subscriber pending queue table. See kernel_hermes.h's own
* doc comments on sk_hermes_queues_boot_init()/SK_HERMES_PENDING_MAX for
* scope and sizing reasoning. A queue is found-or-created lazily by
* doc comment on sk_hermes_queues_boot_init() for scope and sizing
* reasoning (each queue's depth grows on demand since Stage M-0; the table
* of queues is still RAM-sized at boot -- S-3's per-VM-tables item). A
* queue is found-or-created lazily by
* vm_id, same shape as stadium.c's quota_slot_for_vm() and session.c's
* session_find() -- not pre-populated at birth like channel membership
* is, since not every VM ever receives a message. */
typedef struct {
VMUuid vm_id;
int in_use;
int slots[SK_HERMES_PENDING_MAX]; /* sk_hermes_msgs[] indices, circular FIFO */
size_t head;
size_t count;
VMUuid vm_id;
int in_use;
uint32_t *slots; /* arena indices, circular FIFO; Stage M-0: heap-grown
* on push (was a fixed [32]) */
size_t cap;
size_t head;
size_t count;
} SkHermesPendingQueue;
static SkHermesPendingQueue *sk_hermes_queues = (SkHermesPendingQueue *)0;
@@ -460,7 +543,15 @@ static SkHermesPendingQueue *find_or_create_queue(VMUuid vm_id) {
}
if (free_slot < 0) return (SkHermesPendingQueue *)0; /* table full */
memset(&sk_hermes_queues[free_slot], 0, sizeof(SkHermesPendingQueue));
{
/* Stage M-0: a reused table entry keeps its grown ring buffer
* (empty, count 0) rather than the memset leaking it. */
uint32_t *keep = sk_hermes_queues[free_slot].slots;
size_t keep_cap = sk_hermes_queues[free_slot].cap;
memset(&sk_hermes_queues[free_slot], 0, sizeof(SkHermesPendingQueue));
sk_hermes_queues[free_slot].slots = keep;
sk_hermes_queues[free_slot].cap = keep_cap;
}
sk_hermes_queues[free_slot].vm_id = vm_id;
sk_hermes_queues[free_slot].in_use = 1;
return &sk_hermes_queues[free_slot];
@@ -473,10 +564,25 @@ static SkHermesPendingQueue *find_or_create_queue(VMUuid vm_id) {
* nothing is pending anywhere, the overwhelmingly common case. */
static int sk_hermes_pending_total = 0;
static int queue_push(SkHermesPendingQueue *q, int msg_index) {
static int queue_push(SkHermesPendingQueue *q, uint32_t msg_index) {
size_t tail;
if (q->count >= SK_HERMES_PENDING_MAX) return -1;
tail = (q->head + q->count) % SK_HERMES_PENDING_MAX;
if (q->count == q->cap) {
/* Stage M-0: grow the ring (doubling), unwrapped into FIFO order
* with head = 0. All-or-nothing: the old ring stays if this fails. */
size_t ncap = q->cap ? q->cap * 2 : 8;
uint32_t *grown = (uint32_t *)kmalloc(ncap * sizeof(uint32_t));
size_t i;
if (!grown) {
sk_hermes_grow_failed("pending queue");
return -1;
}
for (i = 0; i < q->count; i++) grown[i] = q->slots[(q->head + i) % q->cap];
if (q->slots) kfree(q->slots);
q->slots = grown;
q->cap = ncap;
q->head = 0;
}
tail = (q->head + q->count) % q->cap;
q->slots[tail] = msg_index;
q->count++;
sk_hermes_pending_total++;
@@ -487,7 +593,7 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
void *payload_addr, uint32_t payload_len) {
SkHermesMessage *msg;
SkHermesPendingQueue *q;
int idx;
uint32_t idx;
/* Task 3.5: the one-block bound, enforced uniformly here whether or
* not payload_addr happens to be a chunk carrier -- see
@@ -526,9 +632,9 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
return -1;
}
idx = (int)(msg - sk_hermes_msgs);
idx = msg->index;
if (queue_push(q, idx) != 0) {
sk_hermes_release(msg); /* roll back -- destination queue full */
sk_hermes_release(msg); /* roll back -- queue couldn't grow (logged, counted) */
return -1;
}
@@ -573,13 +679,13 @@ int sk_hermes_pending_count(VMUuid vm_id) {
SkHermesMessage *sk_hermes_pending_peek(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
if (!q || q->count == 0) return (SkHermesMessage *)0;
return &sk_hermes_msgs[q->slots[q->head]];
return msg_at(q->slots[q->head]);
}
int sk_hermes_pending_pop(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
if (!q || q->count == 0) return -1;
q->head = (q->head + 1) % SK_HERMES_PENDING_MAX;
q->head = (q->head + 1) % q->cap;
q->count--;
sk_hermes_pending_total--;
return 0;
@@ -692,18 +798,40 @@ uint32_t sk_hermes_chunk_count(uint32_t len) {
return (len + SK_HERMES_CHUNK_MAX_SLICE - 1) / SK_HERMES_CHUNK_MAX_SLICE;
}
static int reassemble_with(SkHermesMessage **chunks, int n_chunks,
uint8_t *out_buf, uint32_t out_buf_cap,
uint32_t *out_len, int *present);
/* Stage M-0: present[] is sized to n_chunks per call (was a fixed
* [SK_HERMES_MSG_MAX], which also capped n_chunks at 32). One exit path so
* the scratch is always freed; the validation itself is unchanged, in
* reassemble_with() below. */
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 *present;
int rc;
if (!chunks || n_chunks <= 0 || !out_buf || !out_len) return -1;
present = (int *)kmalloc((size_t)n_chunks * sizeof(int));
if (!present) {
sk_hermes_grow_failed("reassembly scratch");
return -1;
}
rc = reassemble_with(chunks, n_chunks, out_buf, out_buf_cap, out_len, present);
kfree(present);
return rc;
}
static int reassemble_with(SkHermesMessage **chunks, int n_chunks,
uint8_t *out_buf, uint32_t out_buf_cap,
uint32_t *out_len, int *present) {
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 ||