From 50047727cf25b84f49872e424228c80dc978db1c Mon Sep 17 00:00:00 2001 From: alice Date: Tue, 1 Sep 2026 20:49:38 +0000 Subject: [PATCH 1/4] scs_member: honest NL-mode DLM registration builder (db20-b/vms-7e2) [wip] scs_member_build_dlm_nl_enq: cat 0x02 op 0x01 ENQ with the lock mode HARD-PINNED to NL (0x00) -- no mode parameter, so a held mode (CR/CW/PR/PW/EX) or an op-07 convert can never be emitted. That construction IS the INV-6 guarantee: OVMX holds no locks, so it registers only NL directory participation in resources the cluster has shown it (never its own held state, of which it has none). Resource name + directory hash come from the discovered record; ungrounded per-message fields zero; value block null. Byte-grounded vs vax3-2to3 (VCC$vSYSDSK1). Unit test asserts NL mode + null VLB + guards. WIP: needs the sender (NL-register each discovered resource on the VAX1 DLM VC, mirroring cm_send_dlm_selfreg) + resource discovery from VAX1's pushed op-0d records, before it is lab-testable. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/vmsscs/include/scs_member.h | 9 ++++++ src/vmsscs/scs_member.c | 52 +++++++++++++++++++++++++++++++++ tests/vmsscs/test_scs_member.c | 37 +++++++++++++++++++++++ 3 files changed, 98 insertions(+) diff --git a/src/vmsscs/include/scs_member.h b/src/vmsscs/include/scs_member.h index b2a6c263..52decb46 100644 --- a/src/vmsscs/include/scs_member.h +++ b/src/vmsscs/include/scs_member.h @@ -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); diff --git a/src/vmsscs/scs_member.c b/src/vmsscs/scs_member.c index add86d31..b4e029c2 100644 --- a/src/vmsscs/scs_member.c +++ b/src/vmsscs/scs_member.c @@ -752,6 +752,58 @@ 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). + * The resource name and its directory hash come from the discovered record; every + * ungrounded 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); /* per-resource directory hash (from the discovered record) */ + body[12] = 0x01; /* node-independent constant */ + put_le16(body + 14, p->member_count); /* 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]) diff --git a/tests/vmsscs/test_scs_member.c b/tests/vmsscs/test_scs_member.c index 265bfee4..7a3de721 100644 --- a/tests/vmsscs/test_scs_member.c +++ b/tests/vmsscs/test_scs_member.c @@ -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(); @@ -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"); From e08d4f5853207bfbc681efe7eb8898fbb6ac8633 Mon Sep 17 00:00:00 2001 From: alice Date: Tue, 1 Sep 2026 21:17:35 +0000 Subject: [PATCH 2/4] scsd: OVMX drives honest NL-mode DLM registration to non-coord members (db20-b) OVMX originated ZERO cat-02 DLM traffic to the non-coordinator member (VAX1) -- only echoes -- so VAX1's directory rebuild with OVMX never drains, and (grounded correlation) VAX1 never advances to blind-probe OVMX's SCA$TRANSPORT SYSAP, the member-STATUS trigger that flips BRK_NON -> counted MEMBER. This drives the missing rebuild participation, honestly. For each op-0d rebuild record a non-coordinator member pushes (which OVMX already echoes), OVMX now also originates an honest NULL-mode registration for that resource: cat-02 op-01 ENQ, mode HARD-PINNED to NL (scs_member_build_dlm_nl_enq has no mode parameter, so a held mode CR/CW/PR/PW/EX or an op-07 convert is structurally un-emittable). NL asserts directory participation holding NOTHING -- INV-6-safe by construction (vms-199). OVMX holds no locks, so it registers only NL, and only resources the cluster has SHOWN it (name + directory hash taken from the echoed record, deduped once per resource); it invents nothing and never claims its own held state, of which it has none. The reference joiner sends 3152 HELD-mode ENQs because it genuinely holds those locks; OVMX sends only the NL subset (455 in the reference). - scs_member_build_dlm_nl_enq (byte-exact unit test, NL-mode + null-VLB asserted) - cm_send_dlm_nl_register (modelled on cm_send_dlm_selfreg) + dlm_nl_reg dedup - discovery hook in the CM_RSP_DLM echo path; SEND SITE TABLE census updated - all 55 vmsscs unit tests pass; scsd_exe builds+links Re-fire grades: does VAX1 now send OVMX a SCA$TRANSPORT CONNECT-REQ? present = this rebuild is the precedent (the SCA$TRANSPORT responder is next); absent = the NL set is not yet enough (iterate count -- NEVER into held modes). Extent is lab-bounded; the NL-only construction is not. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01PhM3QcmFEY3p8YNnHGaYwP --- src/vmsscs/scsd.c | 117 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/src/vmsscs/scsd.c b/src/vmsscs/scsd.c index 0ea23421..712dd889 100644 --- a/src/vmsscs/scsd.c +++ b/src/vmsscs/scsd.c @@ -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. * @@ -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][16]; + 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, @@ -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 * @@ -6477,6 +6488,80 @@ static int cm_send_dlm_selfreg(int sock, int ifindex, struct peer_state *ps, return 0; } +/* + * dlm_nl_reg_add - record that we have NL-registered resource `res16` (the 16-byte + * fixed name field) 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 res16[16]) +{ + for (int i = 0; i < ps->dlm_nl_reg_n; i++) { + if (memcmp(ps->dlm_nl_reg[i], res16, 16) == 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++], res16, 16); + 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; /* the count the member showed us (honest) */ + 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. @@ -7927,6 +8012,38 @@ 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 and + * its directory hash come from the record we were shown. */ + if (cm_shape == CM_RSP_DLM && + !cm_peer_is_coordinator(rx->peers, ps)) { + const uint8_t *qb = buf + 72; + char res16[16]; + memcpy(res16, qb + 20, 16); /* op-0d resource-name field */ + /* real resource names begin with a letter (VCC$, CACHE$, + * F11B$, SCS$...); anything else is not a name -- skip. */ + if (res16[0] >= 'A' && res16[0] <= 'Z' && + dlm_nl_reg_add(ps, res16)) { + uint8_t nl = 16; + while (nl > 0 && (res16[nl - 1] == ' ' || + res16[nl - 1] == '\0')) { + nl--; + } + uint16_t dh = (uint16_t)qb[10] | + ((uint16_t)qb[11] << 8); + uint16_t mc = (uint16_t)qb[14] | + ((uint16_t)qb[15] << 8); + cm_send_dlm_nl_register(rx->sock, (int)rx->ifindex, + ps, rx->our_hw_mac, + rx->our_src_logical, + dh, mc, res16, nl); + } + } } } } From 5bdbad43db8c9caa8b96b1083d490892e1d6bf83 Mon Sep 17 00:00:00 2001 From: alice Date: Tue, 1 Sep 2026 22:27:12 +0000 Subject: [PATCH 3/4] scsd(db20-b): read op-0d resource name from body[48], not body[20] The discovery hook that originates NL-mode DLM registration read the resource name from body[20] of VAX1's echoed op-0d rebuild record. body[20] is SCS_DLM_B_MASTER_CSID -- a u32 CSID whose first byte is never A-Z -- so the A-Z name guard rejected EVERY record and no NL registration ever fired (lab grade: hook precondition met, OVMX echoed VAX1's op-0d 177x, yet DLMNLREG=0). The real resource name is ASCII at SCS_DLM_B_RESNAM (48), corroborated three ways: the DLM field map (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). - Extract the read into a pure dlm_op0d_resname() helper so the offset is unit-testable -- the origin bug was an untested inline offset that failed silently at runtime. test_scsd_wire now pins body[48] and rejects body[20]. - A VMS resource name is 1..31 bytes; widen the per-member dedup buffer 16 -> 32 so full-width names are not truncated. - dir_hash / member_count: their op-0d READ offsets are not yet ground- truthed (the emit-side op-01 uses body[10]/[14]; VAX1's op-0d layout is unconfirmed, cksum sits at body[6:8]). They do NOT gate whether the NL registration fires -- read provisionally, flagged for pcap confirmation. Standalone extractor logic: 4/4 checks. scsd.c -fsyntax-only clean. Tracks vms-7e2 (db20-b). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01PhM3QcmFEY3p8YNnHGaYwP --- src/vmsscs/scsd.c | 75 +++++++++++++++++++++++++---------- tests/vmsscs/test_scsd_wire.c | 48 ++++++++++++++++++++++ 2 files changed, 103 insertions(+), 20 deletions(-) diff --git a/src/vmsscs/scsd.c b/src/vmsscs/scsd.c index 712dd889..587e0125 100644 --- a/src/vmsscs/scsd.c +++ b/src/vmsscs/scsd.c @@ -1053,7 +1053,7 @@ struct peer_state { 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][16]; + 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 @@ -6489,22 +6489,52 @@ static int cm_send_dlm_selfreg(int sock, int ifindex, struct peer_state *ps, } /* - * dlm_nl_reg_add - record that we have NL-registered resource `res16` (the 16-byte - * fixed name field) 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). + * 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 int dlm_nl_reg_add(struct peer_state *ps, const char res16[16]) +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], res16, 16) == 0) { + 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++], res16, 16); + memcpy(ps->dlm_nl_reg[ps->dlm_nl_reg_n++], res, 32); return 1; } @@ -8023,17 +8053,22 @@ static void scsd_sysap_msg_input(struct scs_cdt *cdt, const void *msg, size_t ms if (cm_shape == CM_RSP_DLM && !cm_peer_is_coordinator(rx->peers, ps)) { const uint8_t *qb = buf + 72; - char res16[16]; - memcpy(res16, qb + 20, 16); /* op-0d resource-name field */ - /* real resource names begin with a letter (VCC$, CACHE$, - * F11B$, SCS$...); anything else is not a name -- skip. */ - if (res16[0] >= 'A' && res16[0] <= 'Z' && - dlm_nl_reg_add(ps, res16)) { - uint8_t nl = 16; - while (nl > 0 && (res16[nl - 1] == ' ' || - res16[nl - 1] == '\0')) { - nl--; - } + /* 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)) { + /* dir_hash / member_count: the op-0d record read + * offsets are NOT yet ground-truthed (the emit-side + * op-01 uses body[10]/[14], but VAX1's op-0d layout + * is unconfirmed -- cksum sits at body[6:8]). These + * fields do NOT gate whether the NL registration + * fires; they refine the emitted frame. Read + * PROVISIONALLY pending pcap confirmation; correct + * the two offsets once the live op-0d field map is + * pinned. */ uint16_t dh = (uint16_t)qb[10] | ((uint16_t)qb[11] << 8); uint16_t mc = (uint16_t)qb[14] | @@ -8041,7 +8076,7 @@ static void scsd_sysap_msg_input(struct scs_cdt *cdt, const void *msg, size_t ms cm_send_dlm_nl_register(rx->sock, (int)rx->ifindex, ps, rx->our_hw_mac, rx->our_src_logical, - dh, mc, res16, nl); + dh, mc, res, nl); } } } diff --git a/tests/vmsscs/test_scsd_wire.c b/tests/vmsscs/test_scsd_wire.c index 89cf988d..3ca1f6ec 100644 --- a/tests/vmsscs/test_scsd_wire.c +++ b/tests/vmsscs/test_scsd_wire.c @@ -10838,6 +10838,52 @@ static void test_dlm_message_reaches_the_lock_handler_over_scs(void) "the decoded value block did not match"); } +/* + * db20-b (vms-7e2): the op-0d resource-name extractor must read body[48] + * (SCS_DLM_B_RESNAM), NOT body[20]. The origin bug read body[20] -- which is + * SCS_DLM_B_MASTER_CSID, a u32 whose first byte is never A-Z -- so the guard + * silently skipped EVERY rebuild record and no NL registration ever fired + * (DLMNLREG=0 in the lab, even with the hook precondition met). This pins the + * offset so that class of bug fails a check instead of vanishing at runtime. + */ +static void test_dlm_op0d_resname_reads_body48(void) +{ + uint8_t body[132]; + memset(body, 0, sizeof(body)); + /* A CSID-shaped nonzero value at body[20] (byte 0 = '@', exactly what the + * live VAX1 op-0d dump showed there) -- the old, wrong read site. */ + body[20] = 0x40; body[21] = 0x00; body[22] = 0x11; body[23] = 0x00; + /* The real resource name at SCS_DLM_B_RESNAM (48), space-padded. */ + const char *name = "VCC$vSYSDSK1"; + memset(body + SCS_DLM_B_RESNAM, ' ', 31); + memcpy(body + SCS_DLM_B_RESNAM, name, strlen(name)); + + char out[32]; + uint8_t nl = dlm_op0d_resname(body, out); + CHECK(nl == (uint8_t)strlen(name), + "dlm_op0d_resname must return the trimmed name length %zu, got %u", + strlen(name), (unsigned)nl); + CHECK(memcmp(out, name, strlen(name)) == 0, + "dlm_op0d_resname must read the name from body[48], not body[20]"); + + /* An empty body[48] (whatever sits at body[20]) is rejected -> the guard + * fires nothing. This is the exact condition the origin bug hit for all 177 + * records: body[48] blank, so no false registration. */ + memset(body + SCS_DLM_B_RESNAM, 0, 31); + memcpy(body + 20, "GARBAGE", 7); + CHECK(dlm_op0d_resname(body, out) == 0, + "dlm_op0d_resname must ignore body[20]; only body[48] holds the name"); + + /* A full-width 31-byte name is preserved (no 16-byte truncation). */ + memset(body, 0, sizeof(body)); + const char *longname = "ABCDEFGHIJKLMNOPQRSTUVWXYZ01234"; /* 31 chars */ + memcpy(body + SCS_DLM_B_RESNAM, longname, 31); + nl = dlm_op0d_resname(body, out); + CHECK(nl == 31 && memcmp(out, longname, 31) == 0, + "dlm_op0d_resname must preserve a full 31-byte name, got len %u", + (unsigned)nl); +} + int main(void) { /* THE FAILURE STREAM, taken before anything can dup2() over fd 2. See @@ -11022,6 +11068,8 @@ int main(void) test_poll_refresh_tick_drives_the_daemon_loop(); /* vms-f61 (spec ยง4(O.21)): the readmission-map verdict classifier. */ test_readmit_verdict_classifies_the_rejoin_frontier(); + /* db20-b (vms-7e2): the op-0d resource-name offset -- body[48], not body[20]. */ + test_dlm_op0d_resname_reads_body48(); CHECK(peer_logical_offset > 0, "the peer-logical offset was never located -- the offset-dependent" From 0be57e4d94fb3793343e480216783d986abfe332 Mon Sep 17 00:00:00 2001 From: alice Date: Tue, 1 Sep 2026 22:34:04 +0000 Subject: [PATCH 4/4] scsd(db20-b): source NL-ENQ member_count from OVMX's own state; zero dir_hash (INV-6) The received VAX1 op-0d is a LOCK record (scs_dlm.h field map: master_lkid@8:12, status@12:16, req_csid@16:20, master_csid@20:24, namelen@24, valblk@32:48, resnam@48:80). It carries NEITHER a member_count NOR a dir_hash field, so the prior provisional reads (qb[14] mid-status, qb[10] mid-master_lkid) were garbage -- and feeding that garbage member_count into the NL ENQ FABRICATED it, an INV-6 violation. - member_count: now sourced from OVMX's OWN true membership state (ovmx_cluster.member_count -- the same fact op-01 PARAMS already emits), never from the peer's record. Our fact, not theirs. - dir_hash: honest ZERO. A SCS$DIRECTORY dir-hash is COMPUTED from the resource name, not echoed from a peer's ENQ; the op-0d record has no such field and OVMX does not compute the VMS hash, so it omits (0) rather than invents (INV-6). Only the resource NAME is sourced from the discovered record; every other field is OVMX's own state or an honest zero. Corrects the now-false "directory hash comes from the discovered record" grounding comment in the builder. Neither field gates whether the registration fires. Grounded from the conductor's live op-0d field-map dump. scsd.c + scs_member.c -fsyntax-only clean. Tracks vms-7e2 (db20-b). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01PhM3QcmFEY3p8YNnHGaYwP --- src/vmsscs/scs_member.c | 15 ++++++++++----- src/vmsscs/scsd.c | 41 +++++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/vmsscs/scs_member.c b/src/vmsscs/scs_member.c index b4e029c2..2a2427c3 100644 --- a/src/vmsscs/scs_member.c +++ b/src/vmsscs/scs_member.c @@ -768,9 +768,14 @@ int scs_member_build_dlm_selfreg(const struct scs_member_params *p, * (it has none). * * Grounded from a VAX3->VAX1 NL op-01 ENQ in vax3-2to3 (resource VCC$vSYSDSK1). - * The resource name and its directory hash come from the discovered record; every - * ungrounded 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. + * 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, @@ -793,9 +798,9 @@ int scs_member_build_dlm_nl_enq(const struct scs_member_params *p, 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); /* per-resource directory hash (from the discovered record) */ + 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); /* post-transition member count */ + 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. */ diff --git a/src/vmsscs/scsd.c b/src/vmsscs/scsd.c index 587e0125..1181b296 100644 --- a/src/vmsscs/scsd.c +++ b/src/vmsscs/scsd.c @@ -6575,7 +6575,10 @@ static int cm_send_dlm_nl_register(int sock, int ifindex, struct peer_state *ps, 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; /* the count the member showed us (honest) */ + 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, @@ -8048,8 +8051,9 @@ static void scsd_sysap_msg_input(struct scs_cdt *cdt, const void *msg, size_t ms * 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 and - * its directory hash come from the record we were shown. */ + * 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; @@ -8060,19 +8064,24 @@ static void scsd_sysap_msg_input(struct scs_cdt *cdt, const void *msg, size_t ms char res[32]; uint8_t nl = dlm_op0d_resname(qb, res); if (nl > 0 && dlm_nl_reg_add(ps, res)) { - /* dir_hash / member_count: the op-0d record read - * offsets are NOT yet ground-truthed (the emit-side - * op-01 uses body[10]/[14], but VAX1's op-0d layout - * is unconfirmed -- cksum sits at body[6:8]). These - * fields do NOT gate whether the NL registration - * fires; they refine the emitted frame. Read - * PROVISIONALLY pending pcap confirmation; correct - * the two offsets once the live op-0d field map is - * pinned. */ - uint16_t dh = (uint16_t)qb[10] | - ((uint16_t)qb[11] << 8); - uint16_t mc = (uint16_t)qb[14] | - ((uint16_t)qb[15] << 8); + /* 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,