Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/vmsscs/include/scs_member.h
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,15 @@ int scs_member_build_dlm_response(const struct scs_member_params *p,
int scs_member_build_dlm_selfreg(const struct scs_member_params *p,
uint8_t out[SCS_MEMBER_FRAME_LEN]);

/* db20-b (vms-7e2): originate an honest NULL-mode DLM registration (cat 0x02 op
* 0x01 ENQ, mode NL) for a discovered resource, toward a non-coordinator member.
* INV-6 by construction: NO mode parameter -- the mode byte is hard-pinned to NL
* (0x00), so a held mode can never be emitted. See scs_member.c. */
int scs_member_build_dlm_nl_enq(const struct scs_member_params *p,
uint16_t dir_hash,
const char *resname, uint8_t namelen,
uint8_t out[SCS_MEMBER_FRAME_LEN]);

/* Current time as a VMS 64-bit absolute time (100 ns since 17-NOV-1858). */
uint64_t scs_member_vms_time_now(void);

Expand Down
57 changes: 57 additions & 0 deletions src/vmsscs/scs_member.c
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,63 @@ int scs_member_build_dlm_selfreg(const struct scs_member_params *p,
return 0;
}

/*
* scs_member_build_dlm_nl_enq - originate an honest NULL-mode (NL) DLM
* registration (category 0x02, op 0x01 ENQ, mode NL) for a resource OVMX has been
* SHOWN, toward a non-coordinator member's DLM VC (db20-b, vms-7e2).
*
* INV-6 GUARANTEE, ENFORCED BY CONSTRUCTION: the lock mode (body[30]) is
* HARD-PINNED to 0x00 (NL / null lock). This function has NO mode parameter and
* can NEVER emit a held mode (CR/CW/PR/PW/EX) or an op-07 convert. NL asserts
* directory participation holding NOTHING -- INV-6-safe by construction (vms-199).
* The reference joiner sends 3152 HELD-mode ENQs to VAX1 because it genuinely
* holds those locks; OVMX holds none, so it registers ONLY NL participation in the
* resources the cluster has shown it (from the pushed op-0d rebuild records OVMX
* already echoes) -- it invents nothing and never claims its own held resources
* (it has none).
*
* Grounded from a VAX3->VAX1 NL op-01 ENQ in vax3-2to3 (resource VCC$vSYSDSK1).
* Only the resource NAME comes from the discovered record. member_count is OVMX's
* OWN true count (the caller sources it from ovmx_cluster.member_count, never from
* the peer's lock record -- which carries no member_count field). dir_hash is an
* honest ZERO: a SCS$DIRECTORY dir-hash is computed from the resource name, not
* echoed from a peer's ENQ, and OVMX does not compute the VMS hash -- so it omits
* it (0) rather than invent one (INV-6). Every other per-message field is zero-
* filled; the value block is null. body[4:8] (per-VC tag + counter) and the SCS
* envelope are minted per send.
*/
int scs_member_build_dlm_nl_enq(const struct scs_member_params *p,
uint16_t dir_hash,
const char *resname, uint8_t namelen,
uint8_t out[SCS_MEMBER_FRAME_LEN])
{
if (p == NULL || out == NULL || resname == NULL) {
return -1;
}
if (namelen == 0 || namelen > 31) { /* a VMS resource name is 1..31 bytes */
return -1;
}
build_common(p, member_config_tmpl, SCS_MEMBER_ENV_CREDIT_CONFIG, out);

uint8_t *body = out + 72;
memset(body + 4, 0, SCS_MEMBER_SCA_LEN - SCS_MEMBER_BODY_OFF - 4);
put_le16(body + 0, p->sysap_send_msg);
put_le16(body + 2, p->sysap_ack_msg);
put_le16(body + 4, p->txn); /* per-VC directory tag (opaque, minted) */
put_le16(body + 6, p->checksum); /* per-VC monotonic counter (opaque, minted) */
body[8] = SCS_MEMBER_CAT_DLM; /* 0x02 */
body[9] = 0x01; /* op 0x01 = ENQ */
put_le16(body + 10, dir_hash); /* SCS$DIRECTORY dir-hash: honest 0 (computed from the name, not echoed; OVMX omits rather than invents -- INV-6) */
body[12] = 0x01; /* node-independent constant */
put_le16(body + 14, p->member_count); /* OVMX's own true post-transition member count */
/* body[16:30] zero -- ungrounded per-message ids (INV-6: never replay VAX3's). */
body[30] = 0x00; /* == NL. HARD-PINNED -- the INV-6 guarantee. */
/* body[31:47] zero. */
body[47] = namelen; /* resource-name length */
memcpy(body + 48, resname, namelen); /* the discovered resource name; value block stays null */
return 0;
}

int scs_member_build_response(const struct scs_member_params *p,
const uint8_t *req_frame, size_t req_len,
uint8_t out[SCS_MEMBER_FRAME_LEN])
Expand Down
161 changes: 161 additions & 0 deletions src/vmsscs/scsd.c
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ static void ovmx_cluster_logical(uint16_t sysid, uint8_t out[6])
/* --- vms-5fe: directed-HELLO / SCS-connect responder state --- */

#define OVMX_MAX_PEERS 4
/* vms-7e2 (db20-b): cap on distinct resources NL-registered to one member. */
#define OVMX_DLM_NL_MAX 48

/* vms-298 / vms-584 item 5: THE CON.ID HIGH WORD IS PER-BOOT, NOT COMPILE-TIME.
*
Expand Down Expand Up @@ -1049,6 +1051,10 @@ struct peer_state {
* body[6:8] counter is ps->own_cksum (shared with the barrier steps); the
* txn tag is the SCSD_DLM_DIR_TAG constant. */
int dlm_selfreg_sent; /* one-shot: self-reg emitted for the current epoch */
/* vms-7e2 (db20-b): resources already NL-registered to this member (dedup --
* register each shown cluster resource once, honest NL participation). */
char dlm_nl_reg[OVMX_DLM_NL_MAX][32];
int dlm_nl_reg_n;
/* rd vms-ec75 (DLM rung H11): DISTRIBUTED DEADLOCK SEARCH. A contender node
* holds one resource (mastered by C) and $ENQs a second that QUEUES behind the
* other contender -- a genuine cross-node wait-for cycle. When the wait queues,
Expand Down Expand Up @@ -3673,6 +3679,11 @@ static ssize_t send_frame_raw(int sock, int ifindex, const uint8_t mac[6],
* between barrier steps 4 and 5 on the one CM send_seq
* stream; a null-value-block directory record, CHOKED
* like every other sequenced VC message
* cm_send_dlm_nl_register() the cat-0x02 op-0x01 NULL-mode DLM registration
* (vms-7e2/db20-b) OVMX originates to a non-coordinator
* member for each cluster resource it is shown -- honest
* NL directory participation (mode hard-pinned NL, holds
* nothing); CHOKED like every other sequenced VC message
* scs_reflect_credit() the op8->op9 / op6->op7 credit handshake reply
* scs_send_disconnect_self() self-directed teardown, hand-built frame
*
Expand Down Expand Up @@ -6477,6 +6488,113 @@ static int cm_send_dlm_selfreg(int sock, int ifindex, struct peer_state *ps,
return 0;
}

/*
* dlm_op0d_resname - extract the DLM lock resource name from a received op-0d
* rebuild record's SYSAP body (body = frame + 72). The name is ASCII at
* SCS_DLM_B_RESNAM (48) -- a VMS resource name of 1..31 bytes, space/NUL-padded
* (corroborated by the DLM field map in scs_dlm.h, scs_member.c's own "RESOURCE
* in ASCII at body[48:]" handler, and a live dump of VAX1's op-0d records:
* VCC$vSYSDSK1, F11B$bSYSDSK1, CACHE$cmSYSDSK1, SYS$_$2$DUA0). Writes a 32-byte
* pad-preserving copy to out[32] (stable dedup key) and returns the trimmed name
* length, or 0 if body[48] does not begin with A-Z (not a resource name).
*
* This is its own function so the offset is unit-testable: the db20-b origin bug
* read the name from body[20] (SCS_DLM_B_MASTER_CSID, a u32 -- never a letter),
* so the guard silently skipped every record and NO NL registration ever fired.
* A pure extractor turns that class of offset error into a failing assertion
* (test_scsd_wire) instead of a silent runtime no-op.
*/
static uint8_t dlm_op0d_resname(const uint8_t *body, char out[32])
{
memset(out, 0, 32);
memcpy(out, body + SCS_DLM_B_RESNAM, 31);
if (!(out[0] >= 'A' && out[0] <= 'Z')) {
return 0;
}
uint8_t nl = 31;
while (nl > 0 && (out[nl - 1] == ' ' || out[nl - 1] == '\0')) {
nl--;
}
return nl;
}

/*
* dlm_nl_reg_add - record that we have NL-registered resource `res` (a 32-byte
* space/NUL-padded name field; a VMS resource name is 1..31 bytes) to this member;
* returns 1 if it is NEW (register it now), 0 if already registered or the per-member
* cap is reached (skip). Dedup so each shown resource is registered once (db20-b).
*/
static int dlm_nl_reg_add(struct peer_state *ps, const char res[32])
{
for (int i = 0; i < ps->dlm_nl_reg_n; i++) {
if (memcmp(ps->dlm_nl_reg[i], res, 32) == 0) {
return 0;
}
}
if (ps->dlm_nl_reg_n >= OVMX_DLM_NL_MAX) {
return 0;
}
memcpy(ps->dlm_nl_reg[ps->dlm_nl_reg_n++], res, 32);
return 1;
}

/*
* cm_send_dlm_nl_register - originate an honest NULL-mode DLM registration (cat
* 0x02 op 0x01 ENQ, mode NL) for a resource a non-coordinator member has shown us
* (db20-b, vms-7e2). Modelled on cm_send_dlm_selfreg; the frame is built by
* scs_member_build_dlm_nl_enq, which HARD-PINS the mode to NL -- OVMX holds no
* locks, so it registers only directory participation (holding nothing), never a
* held-mode ENQ or an op-07 convert. The reference joiner runs this rebuild
* exchange with each non-coordinator member; VAX1's SCA$TRANSPORT probe (the
* member-set trigger) follows it draining.
*/
static int cm_send_dlm_nl_register(int sock, int ifindex, struct peer_state *ps,
const uint8_t our_hw_mac[6],
const uint8_t our_src_logical[6],
uint16_t dir_hash, uint16_t member_count,
const char *resname, uint8_t namelen)
{
if (ps->cm_local_conid == 0) {
return 0;
}
struct scs_member_params bp;
memset(&bp, 0, sizeof(bp));
memcpy(bp.dst_mac, ps_port_addr(ps), 6);
memcpy(bp.src_mac, our_hw_mac, 6);
memcpy(bp.src_logical, our_src_logical, 6);
memcpy(bp.peer_logical, ps_sys_addr(ps), 6);
bp.remote_conid = ps->cm_remote_conid;
bp.local_conid = ps->cm_local_conid;
bp.incarnation = ps->incarnation;
bp.recv_ack = ps->vc.seq.recv_seq;
bp.send_seq = scs_seq_advance(&ps->vc.seq);
if (ps->sysap_send == 0) {
ps->sysap_send = 1;
}
bp.sysap_send_msg = ps->sysap_send++;
bp.sysap_ack_msg = ps->sysap_recv;
bp.txn = SCSD_DLM_DIR_TAG; /* per-VC directory tree tag */
bp.checksum = ++ps->own_cksum; /* per-VC monotonic counter, contiguous */
bp.member_count = member_count; /* OVMX's OWN true membership count (caller
* passes ovmx_cluster.member_count) -- NOT
* read from the peer's lock record, which has
* no such field. INV-6: our fact, not theirs. */
uint8_t dframe[SCS_MEMBER_FRAME_LEN];
if (scs_member_build_dlm_nl_enq(&bp, dir_hash, resname, namelen, dframe) == 0 &&
send_frame_vc(sock, ifindex, ps, ps->pb,
"CM DLM NL registration (cat 0x02 op 0x01, mode NL)",
dframe, sizeof(dframe)) > 0) {
scs_vc_record_sent(&ps->vc, bp.send_seq, monotonic_ms());
log_ts(stdout);
printf(" SCSD-I-DLMNLREG, originated NL-mode DLM registration"
" (cat 0x02 op 0x01 NL, res='%.*s' hash=0x%04x cksum=0x%04x seq=%u)\n",
(int)namelen, resname, dir_hash, bp.checksum, bp.send_seq);
fflush(stdout);
return 1;
}
return 0;
}

/*
* cm_send_ack - emit one category-0x04 SYSAP acknowledgement naming our current
* high-water mark on the VC the connection-manager dialogue is riding.
Expand Down Expand Up @@ -7927,6 +8045,49 @@ static void scsd_sysap_msg_input(struct scs_cdt *cdt, const void *msg, size_t ms
cm_shape == CM_RSP_TOKEN ? "token-only" : (cm_shape == CM_RSP_DLM ? "dlm-echo" : "echoed"),
mp.sysap_send_msg, mp.sysap_ack_msg);
fflush(stdout);

/* vms-7e2 (db20-b): having echoed a non-coordinator
* member's op-0d rebuild record, register honest NL
* participation in that resource -- the directory-rebuild
* exchange the member needs before it advances to the
* SCA$TRANSPORT member-STATUS probe. NL only (holding
* nothing); OVMX invents nothing -- the resource NAME comes
* from the record we were shown, and every other field is
* either OVMX's own true state or an honest zero (INV-6). */
if (cm_shape == CM_RSP_DLM &&
!cm_peer_is_coordinator(rx->peers, ps)) {
const uint8_t *qb = buf + 72;
/* Resource name is ASCII at body[48] (see
* dlm_op0d_resname); nl==0 means body[48] is not a
* plausible name -- skip. Dedup so each shown resource
* registers once. */
char res[32];
uint8_t nl = dlm_op0d_resname(qb, res);
if (nl > 0 && dlm_nl_reg_add(ps, res)) {
/* member_count: OVMX's OWN true membership count,
* NEVER the wire. The received op-0d is a LOCK
* record (scs_dlm.h: master_lkid@8:12, status@12:16,
* resnam@48:80) -- it carries NO member_count field,
* so any read from it would FABRICATE the count
* (INV-6 violation). member_count is a membership
* fact OVMX already holds (ovmx_cluster.member_count,
* the same source op-01 PARAMS uses).
*
* dir_hash: honest ZERO. A SCS$DIRECTORY dir-hash is
* COMPUTED from the resource name, not carried in a
* peer's lock ENQ; the op-0d record has no such field
* to echo, and OVMX does not (yet) compute the VMS
* hash. 0 is the ungrounded sentinel -- honest
* omission over an invented value (INV-6). */
uint16_t dh = 0;
uint16_t mc = ovmx_cluster.known ?
ovmx_cluster.member_count : 0;
cm_send_dlm_nl_register(rx->sock, (int)rx->ifindex,
ps, rx->our_hw_mac,
rx->our_src_logical,
dh, mc, res, nl);
}
}
}
}
}
Expand Down
37 changes: 37 additions & 0 deletions tests/vmsscs/test_scs_member.c
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,42 @@ static void test_dlm_selfreg_null_guards(void)
CHECK(scs_member_build_dlm_selfreg(&mp, NULL) == -1, "build_dlm_selfreg NULL out");
}

/* db20-b: the honest NL-mode DLM registration. The mode byte MUST be NL (0x00) --
* the INV-6 guarantee -- and the frame is cat 0x02 op 0x01 ENQ carrying the
* discovered resource name; every ungrounded field is zero. */
static void test_dlm_nl_enq_is_null_mode(void)
{
struct scs_member_params mp;
joiner_params(&mp, 0, 0, 0x00d8, 0x00de); /* sysap send/ack */
mp.member_count = 3;
mp.txn = 0x0004;
mp.checksum = 0x07f4;

uint8_t out[SCS_MEMBER_FRAME_LEN];
const char *res = "VCC$vSYSDSK1";
CHECK(scs_member_build_dlm_nl_enq(&mp, 0x5041, res, (uint8_t)strlen(res), out) == 0,
"build_dlm_nl_enq ok");
const uint8_t *body = out + 14 + SCS_MEMBER_BODY_OFF; /* SCA[58] = abs 72 */

CHECK(body[8] == 0x02, "cat 0x02 (DLM)");
CHECK(body[9] == 0x01, "op 0x01 (ENQ)");
CHECK(body[30] == 0x00, "body[30] == NL (0x00) -- INV-6 guarantee: never a held mode");
CHECK(body[10] == 0x41 && body[11] == 0x50, "body[10:12] = per-resource directory hash");
CHECK(body[14] == 0x03 && body[15] == 0x00, "body[14:16] = member count");
CHECK(body[47] == (uint8_t)strlen(res), "body[47] = resource-name length");
CHECK(memcmp(body + 48, res, strlen(res)) == 0, "body[48:] = discovered resource name");
/* value block after the name is null. */
int vlb_nz = 0;
for (size_t i = 48 + strlen(res); i < (size_t)(SCS_MEMBER_SCA_LEN - SCS_MEMBER_BODY_OFF); i++) {
if (body[i] != 0) { vlb_nz = 1; }
}
CHECK(!vlb_nz, "value block null (NL asserts no held value)");
/* the builder has NO mode parameter, so a held mode is structurally un-emittable. */
CHECK(scs_member_build_dlm_nl_enq(NULL, 0, res, 4, out) == -1, "nl_enq NULL p");
CHECK(scs_member_build_dlm_nl_enq(&mp, 0, res, 0, out) == -1, "nl_enq rejects zero namelen");
CHECK(scs_member_build_dlm_nl_enq(&mp, 0, res, 32, out) == -1, "nl_enq rejects oversized namelen");
}

int main(void)
{
test_op14_byte_exact();
Expand All @@ -974,6 +1010,7 @@ int main(void)
test_params_member_vs_joiner_form();
test_dlm_selfreg_byte_exact();
test_dlm_selfreg_null_guards();
test_dlm_nl_enq_is_null_mode();

if (failures == 0) {
printf("test_scs_member: ALL PASSED\n");
Expand Down
Loading
Loading