Executive-resident process table: a VMS process name survives exec and is visible to other processes (vms-8019) - #12
Merged
Merged
Conversation
…v/vms Adds a shared process table owned by vms.ko so a VMS process name is observable from OTHER processes, replacing nothing yet -- the userspace consumers (sys$getjpi, dcl_cmd_show.c, sys$creprc) are deliberately NOT converted to readers, because vms_kif_register() still has zero callers (vms-9fc) and converting them today would either need a per-process fallback (forbidden, Rule 11) or break the host ctest suite where /dev/vms does not exist. RESCUED COMMIT: the implementing agent's sandbox denied all git commands, so this was committed by the orchestrator from the agent's worktree. NOT PROVEN: the A-writes/B-reads QEMU gate has never been executed -- the agent was also denied qemu-system-aarch64 and ctest. vms.ko is UNBUILT, so the three new vms_module.c hunks are not even compile-checked. This branch is partial work published for continuation, not a merge candidate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The executive process table returned SS$_DUPLNAM 434, SS$_IVLOGNAM 596
and SS$_NONEXPR 2540. All three were wrong, and the tree already carried
the tell: ssdef.h justified SS$_POWERFAIL 598 with "596 taken by
SS$_IVLOGNAM" while noting VMS puts POWERFAIL at 0x254 -- which IS 596.
At least one of the two had to be wrong; both were.
Pinned on the reference lab, OpenVMS VAX V7.3 node VAX1, 2026-07-30, by
two independent documented-tool observations:
$ LIBRARY/EXTRACT=$SSDEF/OUTPUT=SYS$SCRATCH:SSDEF.MAR -
SYS$LIBRARY:STARLET.MLB
$ SEARCH SYS$SCRATCH:SSDEF.MAR "IVLOGNAM","DUPLNAM","NONEXPR",-
"VOLINV","POWERFAIL"
$EQU SS$_DUPLNAM 148
$EQU SS$_IVLOGNAM 340
$EQU SS$_VOLINV 596
$EQU SS$_POWERFAIL 868
$EQU SS$_NONEXPR 2280
$ WRITE SYS$OUTPUT F$MESSAGE(148) %SYSTEM-F-DUPLNAM, duplicate name
$ WRITE SYS$OUTPUT F$MESSAGE(340) %SYSTEM-F-IVLOGNAM, invalid logical name
$ WRITE SYS$OUTPUT F$MESSAGE(434) %SYSTEM-E-NOIOCHAN, no I/O channel available
$ WRITE SYS$OUTPUT F$MESSAGE(596) %SYSTEM-F-VOLINV, volume is not software enabled
$ WRITE SYS$OUTPUT F$MESSAGE(868) %SYSTEM-F-POWERFAIL, power failure occurred
$ WRITE SYS$OUTPUT F$MESSAGE(2280) %SYSTEM-W-NONEXPR, nonexistent process
$ WRITE SYS$OUTPUT F$MESSAGE(2540) %SYSTEM-F-RIGHTSFULL, rights list is full
So 596 is VOLINV, not IVLOGNAM; 598 is VOLINV re-severitied rather than
a distinct condition; and the displacement note was built on sand.
The CHOICE of SS$_IVLOGNAM for a malformed process name -- which reads
oddly, being named for logical names -- is oracle-pinned too, but
behaviourally rather than by symbol:
$ SET PROCESS/NAME="THISNAMEISWAYTOOLONG"
%SET-E-NOTSET, error modifying process name
-SYSTEM-F-IVLOGNAM, invalid logical name
Severities in the DCL message table are corrected from the same
observations (DUPLNAM and IVLOGNAM are F, NONEXPR is W; the 596 slot was
mislabelled IVLOGNAM and is now VOLINV).
Scope: only the constants this facility returns, plus SS$_POWERFAIL,
whose sole documented rationale was the now-retired 596 collision. The
rest of ssdef.h remains unpinned -- that is vms-c90.
vms_internal.h also gains the vms_proc_free_claimed() declaration used
by the reaper fix later on this branch; it is inert here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vms_proctab.c asserts in code and in three comments that process-name
uniqueness AND lookup are scoped to the UIC group, but every process in
test_kmod_procnam ran as root, so uic_group was 0 everywhere and the
group comparison was trivially true. The test printed an identical 15/15
with the comparison deleted outright: the one piece of VMS fidelity the
file argues hardest for had zero discriminating coverage. That is the
same shape as the facade class this item exists to kill -- correct-
looking output no test can tell from the wrong implementation.
The scoping itself is now oracle-pinned, on the reference lab OpenVMS
VAX V7.3 node VAX1, 2026-07-30, with three detached processes taking the
SAME process name under different UICs:
$ RUN/DETACHED/UIC=[300,1]/PROCESS_NAME=OVMXDUP ... LOGINOUT.EXE
%RUN-S-PROC_ID, identification of created process is 20200220
$ RUN/DETACHED/UIC=[301,1]/PROCESS_NAME=OVMXDUP ... LOGINOUT.EXE
%RUN-S-PROC_ID, identification of created process is 20200221
$ RUN/DETACHED/UIC=[300,2]/PROCESS_NAME=OVMXDUP ... LOGINOUT.EXE
%RUN-F-CREPRC, process creation failed
-SYSTEM-F-DUPLNAM, duplicate name
Same name, different group: accepted. Same name, same group: refused
with SS$_DUPLNAM. (Both survivors were confirmed alive in SHOW SYSTEM
first, so the acceptance is not an artifact of the first one exiting.)
The test now forks a helper that setgid()s to group 300 before
registering -- staying uid 0, so it keeps the privilege to open
/dev/vms, but landing in a different UIC group, which the executive
derives from its credentials rather than taking on its word. Three
assertions discriminate:
A a name group 0 holds is FREE in group 300 (group-blind: DUPLNAM)
B the helper resolves that shared name to ITSELF, not to group 0's row
C from group 0, a name held only in group 300 does NOT resolve
(group-blind: SS$_NORMAL)
Also locks the process-table ioctl ABI with _Static_asserts on struct
sizes and on the encoded ioctl numbers. The kernel side of vms_ioctl.h
takes _IOWR from <linux/ioctl.h> while vms_kif.c gets the hand-rolled
fallback at the top of the header (it includes no <sys/ioctl.h>), and
the QEMU proof has only ever been RUN on aarch64 -- the x86_64 half of
that agreement was an assumption. The assertions are evaluated by every
translation unit that includes the header, so the CI x86_64 build proves
the layout on an architecture this host cannot execute.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vms_proc_reap_dead() selected a victim under vms_proc_hash_lock, dropped the lock, then called vms_proc_free(victim) -- which reads victim->hash_node to claim the entry. In that gap a concurrent vms_dev_release() of an exiting task could claim the same entry and kfree_rcu() it, so the claim's own hlist_unhashed() guard was read from potentially freed memory. Narrow (reaping only touches entries whose task is already released) but real, and -smp 1 under QEMU cannot surface it, so no test would ever have caught it. The unlink IS the ownership claim, so it must happen while the lock is still held. vms_proc_free() is split: vms_proc_free_claimed() does the teardown of an entry the caller has already unlinked, and vms_proc_free() stays the claim-then-tear-down entry point for callers that still hold nothing. The reaper hash_del_rcu()s inside its own locked traversal and then calls the claimed variant, so no other path can be mid-free on an entry it is looking at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The kernel-executive-negative-control job asserts the exact split init.sh reports when /dev/vms is absent. tests/qemu/init.sh auto-discovers /tests/test_kmod_*, so adding test_kmod_procnam made an 11th suite that correctly fails without the executive -- the harness now reports "3 suites passed, 8 suites failed" and the pinned literal 7 sent the job red on every push. Proven by execution here, not by reading: podman build -f tests/qemu/Dockerfile --build-arg NEGATIVE_CONTROL=1 podman run --rm <image> => "=== FINAL RESULTS: 3 suites passed, 8 suites failed ===" and the three greps this job runs -- 'cannot open /dev/vms', the FINAL RESULTS literal, and 'PASS: vmsfs.ko loaded, filesystem registered' -- all match on that output with 8 and none match with 7. The comment now states the maintenance convention this literal encodes, because the obvious "fix" is to relax it into a range or a regex: an executive-dependent suite that silently stopped depending on /dev/vms is exactly what this job exists to catch, so the literal has to be edited by hand every time the suite list changes. The brittleness is the feature. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pinning SS$_POWERFAIL 868, SS$_NONEXPR 2280 and SS$_UNASEFC 564 landed
each of them on top of a placeholder that already held that value, so
the source-of-truth header ended up asserting that two distinct VMS
conditions share one status:
SS$_POWERFAIL 868 == SS$_SYNCH 868
SS$_NONEXPR 2280 == SS$_UNASEFC 2280
SS$_UNASEFC 564 == SS$_IVSECFLG 564 (created by fixing the second)
A header that permits this is not merely untidy: `status == SS$_SYNCH`
would also have matched SS$_POWERFAIL. This is the pinning commit's own
blast radius, not the vms-c90 constants sweep.
Every displaced symbol is pinned on the same oracle by the same two
independent documented-tool methods (reference lab node VAX1, OpenVMS
VAX V7.3, 2026-07-30), driven and read back by me rather than taken from
a report:
LIBRARY/EXTRACT=$SSDEF ... SYS$LIBRARY:STARLET.MLB + SEARCH
$EQU SS$_IVSECFLG 364
$EQU SS$_UNASEFC 564
$EQU SS$_SYNCH 1673
F$MESSAGE(364) -> %SYSTEM-F-IVSECFLG, invalid process or global section flags
F$MESSAGE(564) -> %SYSTEM-F-UNASEFC, unassociated event flag cluster
F$MESSAGE(1673) -> %SYSTEM-S-SYNCH, synchronous successful completion
SS$_SYNCH at 1673 is still a success status (1673 & 7 == 1), so it stays
in the success section on its own merits rather than by placement.
The only remaining duplicate value in the header is the intentional
SS$_NORMAL == SS$_CONTINUE == 1. Checked mechanically, not by eye:
grep -oP '^#define\s+SS\$_\w+\s+\K\d+' ssdef.h | sort -n | uniq -d
prints exactly "1".
The section headings are now noted as unreliable severity labels -- some
pinned values sit under a heading their real severity contradicts.
Reconciling the headings and the remaining unpinned constants stays
vms-c90; this commit only removes the contradictions it introduced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ncate it
The userspace client copied an inbound process name into a
VMS_PRCNAM_SIZE field before handing it to /dev/vms, so the executive
never saw an oversized name at all. vms_kif_setprn("THISNAMEISWAYTOOLONG")
returned SS$_NORMAL and the executive then held "THISNAMEISWAYTO" -- a
process silently named something the caller never asked for. The
$GETJPI name selector had the same clip and was worse: an oversized
lookup key resolved whatever process holds its first 15 characters, so
the call answered for a DIFFERENT process instead of refusing.
That is the illegal third answer: a plausible-looking handler (truncate
and succeed) for a condition VMS refuses outright. The oracle
transcript saying so was already quoted in this tree while the code did
the opposite of what it quoted.
Oracle, driven and read back here (VAX1, OpenVMS VAX V7.3, 2026-07-30):
$ SET PROCESS/NAME="IMPL8019NAM15X" ! 14 chars -> accepted
$ SET PROCESS/NAME="IMPL8019NAM15XY" ! 15 chars -> accepted
$ SET PROCESS/NAME="IMPL8019NAM15XYZ" ! 16 chars
%SET-E-NOTSET, error modifying process name
-SYSTEM-F-IVLOGNAM, invalid logical name
$ WRITE SYS$OUTPUT F$GETJPI("","PRCNAM")
IMPL8019NAM15XY ! UNCHANGED
So the boundary is exactly 15, the answer is SS$_IVLOGNAM (340, pinned),
and the existing name is left alone -- VMS does not truncate and does
not partially apply.
An inbound name now travels in VMS_PRCNAM_XFER (64), a buffer strictly
larger than the longest legal name, so an oversized name arrives intact
and name_is_valid() -- which inspects only the first VMS_PRCNAM_SIZE
bytes -- rejects it. The $GETJPI key moves out of info.prcnam into its
own sel_prcnam field, making info.prcnam output-only; a row's name is
never also the lookup key. The userspace copy is still bounded, but the
bound cannot manufacture a legal name, and a _Static_assert holds
VMS_PRCNAM_XFER > VMS_PRCNAM_SIZE so it never can. This is an OVMX wire
choice, labelled as one in the header -- VMS publishes no ioctl.
The struct sizes and therefore the ioctl numbers change (SETPRN
0xC0185641 -> 0xC0485641, GETJPI 0xC0305642 -> 0xC0705642). The ABI
_Static_asserts move with them; verified by compiling the header
natively and printing the values, which match the new literals.
Test section 8 is what tells the two implementations apart, using a
16-character name whose first 15 characters are the name the process
already holds -- so a truncating client returns SS$_NORMAL for it (no
clash with anybody) while the executive returns SS$_IVLOGNAM. It also
asserts the name is unchanged after each rejection, that a 200+
character name past the transfer buffer is rejected too, that an
oversized lookup key does not resolve the holder of its prefix, and, as
a control, that the legal 15-character key still resolves.
Ran in the QEMU harness against a real /dev/vms on aarch64:
test_kmod_procnam: 30 passed, 0 failed (was 22)
FINAL RESULTS: 11 suites passed, 0 suites failed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
baron-3dl
added a commit
that referenced
this pull request
Jul 30, 2026
Third rebase of PR #6. main advanced to 00f426c (vms-8019, "Executive-resident process table", PR #12). src/libvmssys/vms_kif.c auto-merged clean: vms-8019's new SETPRN/GETJPI/PROCSCAN wrappers carry no vms_dev_fd absence guard, so nothing this branch deletes came back. The one conflict is the kernel-executive negative-control literal in ci.yml, and the textual merge of it was WRONG in a way that would have silently weakened the control. Both branches independently moved the executive-absent split from "3 passed, 7 failed" to "3 passed, 8 failed" -- main by adding test_kmod_procnam, this branch by adding test_kmod_pin, both executive- dependent. Merged, BOTH suites exist, so the real split is 3 passed / 9 failed. Taking either side verbatim would have left a stale 8 that no longer describes reality; a stale count here is exactly the silent weakening the control exists to prevent. Resolved to 9, and the comment now records the arithmetic and the per-item history so the next merge sees the trap coming.
This was referenced Aug 7, 2026
Closed
baron-3dl
added a commit
that referenced
this pull request
Aug 7, 2026
Merging with four inherited failures, each verified red on main's own run 31140591441 at the SHA this branch forked from. Nothing green on main is red here. Build & Test comparison (main job 92749473589 vs PR job 92907044517): main: 2 failures -- #12 userspace_service_register_negctl (852.01s) AND #24 x86_64_reloc_survey_fresh PR: 1 failure -- #24 x86_64_reloc_survey_fresh only; #12 now Passed (792.97s) ctest #12 is HEALED by vms-0c3. ctest #24 (labelled 'integration toolchain') is pre-existing and in the same family as the three IMGACT/link job failures -- another session's area, untouched by this batch. Kernel Executive Per-Facility Negative Controls: all 6 shards green here; shard 0 is RED on main (bind-client-no-register, test_syssvc_setuai undeclared). Healed by vms-570. Inherited-red jobs: OVMX_IMGACT Build Mode (musl), OVMX_IMGACT Build + Activate (musl, x86_64), Multi-Object main() Executable Link + Activate x86_64 (vms-206), and Build & Test via ctest #24.
baron-3dl
added a commit
that referenced
this pull request
Aug 7, 2026
…negctl fixture, attribution race) (#164) Four tests were red on main. Three were deterministic reds hidden behind "flaky"/unowned labels; one was a genuine concurrency race. Full suite is 113/113 green under `ctest -j` after these fixes. x86_64_reloc_survey_fresh (#24): docs/design-link-x86_64-relocs.md went stale after sys_uai.c pulled in vms_kif.h. The survey's include list is built from src/<c>/include per component, and src/libvmssys has no include/ subdir, so 8 libvmssys-dependent TUs failed to compile standalone and silently dropped from the surveyed set. Add -I$REPO/src/libvmssys and regenerate (72obj/13fail -> 80obj/5fail; remaining 5 are unrelated missing-header cases). awk/tooling logic otherwise unchanged. userspace_service_register (#11): the gate walked $SRC_ROOT with a raw `find`, descending into gitignored .claude/worktrees/ (parallel repo checkouts) and reading their add_subdirectory()/install() as this tree's, flagging escapes that belong to stale checkouts. Scope all three CMakeLists.txt walks to git-tracked files via `git ls-files`, with a .claude-pruned `find` fallback for non-git trees. "The product tree" == tracked source; not a silent skip. userspace_service_register_negctl (#12): two stacked bugs in the register_buildset.awk PARTIAL-parse control, deterministically red (identical on a pristine HEAD worktree), not flaky. (a) The malformed fixture did `sed '$d'`, deleting the trailing array ']' instead of the entry's '}', so the parser's "file"-vs-"}" balance stayed even and never tripped. (b) Once (a) was fixed the awk correctly refused with its diagnostic on stderr (by design; the real gate splits stdout/stderr), but the control captured stdout only, so its "named reason" grep saw an empty string. Fix both; register_buildset.awk is unchanged. facility_attribution_negctl (#111): section G derived the host site list by running the instrument against the LIVE shared checkout, then diffed it against the container side — a snapshot frozen into the image at build. A concurrent write to the checkout mid-run (another test or another session) skewed the diff, producing a different spurious mismatch every run (the "different symptom every run" flaky signature). Run the frozen $WORK/pristine snapshot on both sides so a real image/tree divergence still reds but a mid-run edit cannot. No test weakened, no retries/sleeps, committed execution record untouched. Closes vms-354, vms-53f, vms-60c, vms-2d4; completes vms-aa6. Co-authored-by: alice <alice@workspace.local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl
pushed a commit
that referenced
this pull request
Aug 8, 2026
…k check
The vms-b5b harness fix added, to all 30 tests/qemu/*.c suites:
setvbuf(stdout, NULL, _IOLBF, 0); /* ... unflushed fork() cannot splice ... */
The literal "fork()" in that comment collides with a gate on CURRENT main:
tests/integration/test_userspace_service_register.sh line 1621 detects a
single-process proof with `grep -q 'fork[[:space:]]*(' "$proof"` on RAW,
un-comment-stripped source (unlike the calls-check at 1634, which runs
strip.awk first). The negctl control at
test_userspace_service_register_negctl.sh:681 redirects the event-flag
services' proof to test_syssvc_ef_local.c (a single-process suite) and
expects "EXECUTIVE DECLARATION WHOSE PROOF IS SINGLE-PROCESS: sys$setef".
Our comment made ef_local.c match the fork grep, so the SINGLE-PROCESS check
was skipped and the gate fell through to "NEVER CALLS THE SERVICE:
sys$ascefc/dacefc/dlcefc" -- ctest #12 userspace_service_register_negctl
went red (green on main, deterministic across two runs).
Reword the comment to say "child process" so it carries no `fork(` token.
setvbuf behavior is unchanged; this restores the gate's fork check to
working correctly (closing the comment-spoof hole the batch opened) without
touching the gate. NOTE for authenticity owner: the fork check greps raw
source and is comment-spoofable in general -- it should strip comments like
the calls-check does (follow-up, out of this batch's scope).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl
added a commit
that referenced
this pull request
Sep 4, 2026
…fect:
SCS hands a SYSAP its 132-byte BODY, the CM codec parsed a 204-byte FRAME
THE WALL, decoded from the live run rather than guessed. On join-e72refire
(2026-09-04, the 2-node VAX cluster) this node's promotion burst reached the
wire and **VAX2 answered 0.7 ms later with the cat-0x01 op-0x03 membership
COMMIT that starts admission** (pcap frame 271, t=18.6853, txn=0005 cks=1475
epoch=7 role=0x20 class=0x02). OVMX never answered it. The CNXTRACE ring says
exactly why, in one record: `ARRIVAL state=ADMIT detail=unparsed aux=0x00000084`
-- 0x84 = 132, the SYSAP body length. Three real inbound CM messages were lost
that way in that one run: VAX1's op-0x01 parameters (t=16.371), VAX2's op-0x01
(t=18.683) and VAX2's op-0x03 COMMIT.
Design sec 3.2.4 is explicit about the receive seam: SCS "calls
scs_sysap_ops.message(ctx, local_conid, frame + 72, inner_len - 16)", and
vms_scs_fsm.c's h_rx_appl_msg does exactly that -- "its own 132 bytes and
nothing below them". FC-P3.15's body-level retrofit converted the
ORIGINATE/RESPOND half and left the PARSE half calling vms_frame_classify(),
which reads the ETHERTYPE at abs 12. A SYSAP never sees one, so every real
inbound CM message was refused as E_NOTSCA. Every host test stayed green
because they fed composed 204-byte frames: they were testing a contract the
executive never uses.
THE FIX, in three parts, all of them consequences of that one boundary:
1. RECEIVE IS BODY-LEVEL. Every parse and every response recipe in
vms_cluster_codec_cm.{c,h} takes the 132-byte body through the VMS_OFB_CM_*
family (the VMS_OFF_CM_* absolute family stays, derived, for the test-only
composer). cm_check_class(fi) becomes cm_check_body(body, len) -- an exact
length gate, because spec sec 4(d) makes the VMS$VAXcluster class fixed.
cnxman_{join,barrier,coord}_rx_frame() become _rx_body().
2. A PARTICIPANT ADDRESSES BY CSB, NOT BY CSID. cnxman_ops_send() resolves a
destination CSID, and vms_cnxman.c's own comment already said "no CSID is
ever learned, so every CSID-addressed origination honestly fails here" --
so the barrier could not send one of its twelve op-0x0b steps, and could
not even acknowledge a transition open. A peer's CSID is not something this
node can learn (E30: op-0x06 carries "a member re-asserting ITS OWN record
or another already-admitted member's"). The CONNECTION is real: book p. 7-23
makes a CSB the record of the SCS connection to that system. New
cnxman_ops.send_csb(csb_index, ...) + `from_csb` through the rx entries;
the barrier keeps `coordinator_csid` as identity and `coordinator_csb` as
address.
3. THE IDENTITY EXCHANGE IS PER-PEER (part A). Decoded from the reference join
vax3-2to3-established-join-20260730: the joiner sent op-0x14+op-0x01 to VAX1
at t+29.8253 AND to VAX2 at t+30.3692, each on that peer's own VC with its
own send-msg# starting at 1, and both members sent theirs back the same way
-- it is what a connection manager says on every VMS$VAXcluster connection,
in both directions. Only op-0x02 is single-coordinator. So it moves out of
the join's step 5 into cnxman_join_advertise_peers(), run on CNXMAN's
once-a-second beat over every CSB the ladder calls OPEN, idempotent per
(peer, connection) via a new CSB `cm_advert_conid`/`cm_advert_sent` pair.
That covers the member whose connection arrived while the join was in IDLE
-- the VAX1 case, i.e. the node CLUSTER_NODES is read from. Its mirror is
fixed too: a peer's PARAMS is now filed in the SENDER's CSB (it used to go
to the join's target), in any state, and vms_cnxman.c records the ack side
once per message on the CSB it really arrived on.
FOUND AND FIXED ALONG THE WAY: a duplicate send-msg# at the exact hand-over
from the join's dialogue to the barrier's. The join advanced the CSB counter
before stamping (correct: first message carries 1); the barrier/coordinator
stamped and let the glue advance after the send. They share a CSB -- the
member a join is driven through is the coordinator whose transition the
barrier answers -- so the first barrier-side body repeated the join's last
number, on the one connection that matters. All originations now go through
cnxman_envelope_originate() (assign, then stamp) and the transport thunks
advance nothing. The R1 test asserts strict monotonicity across the whole
drive and REDDENS when the fix is reverted (verified).
INV-6: nothing here manufactures membership. The only route to MEMBER remains
a real op-0x06 carrying a shape-valid coordinator CSID, from which the
generation is read off the wire and this node's own CSID computed from its own
real SCSSYSTEMID. test_member_only_on_a_real_op06_csid drives the FULL
completion -- COMMIT, rebuilds, membership burst, transition open, GO, twelve
barrier steps, release #12 -- with an op-0x06 carrying no valid CSID and
asserts the barrier completes (an unanswered obligation strands the
coordinator) AND the local CSID is still unlearned. The per-peer beat is
asserted over 200 runs to send four records and touch no membership cell.
TESTS: 60/60 cluster + 236/236 full + 109/109 unit; test_cnxman_join 388
checks (was 340). New: the seam assertion (a BODY parses and is echoed; a
whole FRAME is honestly NOT_MINE), the post-ADMIT drive to MEMBER op by op,
the MEMBER-only-on-a-real-CSID negative, and four per-peer tests. Gate OK (73
files). NetBSD-VAX elf32 ALL PROOFS PASSED, Linux vms.ko clean.
KNOWN TWIN, NAMED NOT HIDDEN: vms_mscp_cl_fsm.c still classifies a frame on a
path SCS feeds a body, so an MSCP END is refused and counted. It cannot block
a join (the disk client is not a membership prerequisite, E68) and converting
that codec touches the MSCP server and block-transfer paths -- its own item.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl
added a commit
that referenced
this pull request
Sep 4, 2026
… CREDIT dialogue on a coalescing quantum of 3, and MEMBER waits for op-0c #12 E78's credit fix worked: against the live cluster the coordinator drove the op-0x06 MEMBERSHIP burst for the first time ever. OVMX answered EVERY record with a cat-0x04 — 254 frames in 31.6 ms, ~8000/s — and VAX2 took a fatal INVEXCEPTN on the interrupt stack and halted. It also reached its own MEMBER state off the CSID in those records, before a single barrier frame existed. THE LAW, MEASURED (vax3-2to3-established-join-20260730, joiner->coordinator): 254 op-0x06 in -> 84 cat-0x04 out. Ratio 3.02. with two independent readings of WHY that agree exactly: the ack-msg word at body[2:4] advances by 3 across 83 of 83 in-burst gaps (never 1, never 2), and the credit field at abs 62 reads 3 on 85 of the 86 carriers on that link. Corroborated corpus-wide by the parallel crash-safety audit at 6549 acks from six responder nodes, advance >= 3 in every one. So the cat-0x04 is a CREDIT CARRIER on a COALESCING QUANTUM of three released buffers — one credit is one released buffer is one consumed peer message, so the credit stamped and the ack advance are one fact seen twice. Both wrong answers are now excluded by an equality assertion, not a bound: one per record (254 frames, the crash) and one per exhausted credit WINDOW (~26, an under-ack by a factor of the grant, which passes any "fewer than N" test). The three opcodes sec 4(o) lists (04/49, 04/00, 04/02) are not three messages and not a phase set: one message from a rotating pool of three recycled buffers (28/34/28 across the burst), body[9] being uninitialised content per sec 4(p) — the 0x49 frame reads "\x04\x49IR_LOOKUP SCS$DIRECTORY". We send zeros. WHAT CHANGED * join_h_membership() CONSUMES: counts the record, learns the CSID, originates nothing. No cat-0x04 is built anywhere in that FSM any more. * scs_fsm_credit_return_due() — new, SCS-owned: has this CDT's ledger reached SCS_CREDIT_COALESCE. The decision is a ledger read; a SYSAP cannot talk it into a carrier it has no credit for. * cnxman_credit_carrier() emits the grounded cat-0x04 on the VC, after the FSMs (so an answer that already piggybacked leaves nothing to carry) and envelope-stamped from the CSB, which is what makes the ack advance real. It is a cat-0x04 and not the op-8 special credit message because the reference emits 0 op-8 on the 204-byte VC class; op-8 stays the backstop. * The p. 2-44 low-credit flush DEFERS for the length of a SYSAP delivery (scs_fsm.delivery_depth) and is re-tested at its end — p. 2-44 is "instead of waiting for a message to ride on", and inside a delivery there may still be one. The teardown's deliberate op-8 does NOT defer (deferring it wedged the directory disconnect behind a timer; caught by test_scs_directory). * MEMBER now turns over ONLY on the barrier's real op-0x0c #12 (cnxman_barrier.commits, moved in barrier_finish and nowhere else), routed as CNXMAN_EV_TRANSITION_DONE. Blocked if this node has no learned CSID (E73's assertion, kept) or if the coordinator's nodemap NAMED us and said no. Nodemap SILENCE promotes and is counted (commits_unmapped): sec 4(p) says the bitmap's width is undetermined, so treating silence as refusal would strand a real join. Phase 2 now reports that answer three-valued. TESTS — the new ones red on either wrong answer, not just on the flood * test_cnxman_op06_flood.c (new): 255 records -> EXACTLY 85 carriers; no carrier stamps below the quantum; the credit returned is quantum*carriers (which finally explains sec 4(h)(1c)'s "last credit never returned", 131/131); the deferral isolated; source scans that the shipping code is built this way. * test_cnxman_join: 255 op-0x06 -> ZERO originations and still ADMIT; MEMBER asserted absent after each of releases 1..11 and present only on #12. * Corrected the tests that encoded the refuted premise (acks_sent==1, CSID_LEARNED->MEMBER). They now assert the measured behaviour. VERIFIED: 63/63 cluster_host+cluster_sim, 112/112 -L unit, 239/239 full ctest, cluster_core_includes_gate OK, elf32-vax cross-compile ALL PROOFS PASSED (vms_scs_fsm/vms_cnxman/vms_cnxman_join_fsm/vms_cnxman_barrier_fsm all ILP32 width-clean). No lab build, no boot, no fabricated membership. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl
added a commit
that referenced
this pull request
Sep 4, 2026
…tum 3 (SCS_CREDIT_COALESCE=3, census-grounded); no flood; MEMBER gated on real barrier op-0c #12; ordering bug fixed. + CRASH-SAFETY GATE merged (9-class taxonomy, 0 FP on 19 ref captures, flags all 3 known crashes, found N2/N3/N5). 63/63+full 239/239+elf32-vax. Next: reset+re-fire (run the gate on the pcap)
baron-3dl
added a commit
that referenced
this pull request
Sep 5, 2026
…DINATOR; both the crash and the stall are one defect (a mandatory field asserted as zero)
REFUTES all three E85 hypotheses. The seq350 close is neither (a) the 17.4s
channel-listen-timeout, nor (b) a peer DISCONNECT, nor (c) OVMX going idle at
the barrier. The pcap says OVMX DID drive its own barrier step 1 (frame 888,
op-0x0b, epoch 5, step 1) and the coordinator DID acknowledge it (frame 890,
0x81/0x0b, 1.2 ms later) -- the drive was never the problem. What happened is
frame 898, at t+23.9535: `msgtype 0xb1` from `08:00:2b:1e:85:61` ("VAX2 ") to
the cluster multicast -- the sec 4(O.30) LAST GASP. The coordinator BUGCHECKED
0.6 ms after OVMX's frame 894, and the station then vanished from the LAN for
~90 s (per-5s per-MAC census: present 0-25 s, absent 25-115 s). CNXTRACE's
`cdt-closed rc=5 (path lost)` at seq350 is OVMX correctly observing a dead
peer. There was no barrier to be idle in: the cluster was already gone.
E83's vc_failover did not fire because ITS PRECONDITION DID NOT HOLD, not
because it is broken: no channel listen-timeout occurred (VAX2 was transmitting
2 ms earlier), and VAX2 presented on exactly ONE channel, so there was no
alternate path to move a circuit to. `vms_pe_fsm.c` is untouched by this
change; the "3 MACs" in the E83 ledger are VAX1's and VAX2's across the run,
not concurrent paths for the dying system.
THE FRAME: 894, `cat-0x86 op-0x00`, the transaction-close RESPONSE, the FIRST
one OVMX ever emitted (254 op-0x06, 85 cat-04 acks, one close). It is the only
OVMX SYSAP frame between the barrier ack and the gasp; the step before it was
acknowledged, i.e. accepted. Spec sec 4(p) already names this response as the
one whose payload bugchecks the peer -- it named the ECHO variant; this is the
other one.
WHAT WAS WRONG WITH IT, MEASURED, NOT GUESSED. Over the whole capture library
(47 pcaps, five distinct responder nodes), of the 122 body offsets in
body[10:132] exactly ONE is nonzero in every real close: `body[24]`, 1308 of
1308, values {1,3,4,5}. Every other offset is zero somewhere -- a real close
with only ONE nonzero byte in that span exists, and that byte is this one.
OVMX sent 0. The predecessor userspace stack sent 3 or 4 there across ~14 runs
and never crashed a peer on a close.
ITS MEANING IS NOT GROUNDED, and this commit does not invent one (Rule 8). Four
derivations were tested and REFUTED, not assumed: it is not an echo (best
matching request offset agrees 358/1308 = 27%, no offset does better, so no
peer assigns it on the wire); it is not the op-0x01 PARAMS block sec 4(p) says
the close carries (every real PARAMS puts 0x0001 there while the same node's
closes put 3, 4 and 5); it is not per-node (every responder emits several
values); and the rest of the close body is demonstrably stale-buffer residue
(real closes carry OPCOM text and lock resource names there), which is what
first made "the payload is don't-care" look safe.
SO THE OMISSION MOVES UP A LEVEL. A fixed-width body cannot express "omitted":
for this field "omit" and "assert zero" are the same bytes, and zero is a value
no node has ever put on this wire. `vms_cm_close_build()` now takes the field
and REFUSES (VMS_CODEC_E_CLASS, writes nothing) when it is 0, so the codec is
structurally incapable of emitting the frame; `join_h_close()` withholds the
response, counts it (`closes_withheld`) and says so once on OPA0:. Not a join
failure -- nothing is broken, this node simply has no value for a mandatory
field and will not invent one (INV-6). `cnxman_join_cfg` carries the two cells
a future grounded source fills, and the host test proves BOTH branches, so
restoring the response is one glue assignment and no other change.
THE SECOND DEFECT, WHICH THE COORDINATOR NEVER LIVED LONG ENOUGH TO SHOW: every
one of OVMX's twelve barrier steps was going out with (txn, token) = (0, 0).
`cm_txn`/`cm_token` were read by the stamper and WRITTEN BY NOBODY in this
executive -- only by three test beds, which is exactly why the R1 suite was
green while the wire carried zeros. Measured per (category, opcode) over every
originated body in the corpus, the opcodes split cleanly in three and the split
is "does the sender expect an answer": always-nonzero (op 0x03 142/142, 0x05
282/282, 0x09 97/97, **0x0b 1035/1035**, cat-02 op 0x0d 21680/21680, cat-06
op 0x00 1361/1361) and always-zero (op 0x00, 0x02 95/95, 0x04, 0x06
12757/12757, 0x0a 125/125, 0x0c 1104/1104, 0x14 203/203, all of cat-04).
Twelve steps sharing one value are twelve requests a coordinator cannot match
to twelve releases -- a strictly worse version of the ordinal collision that
already stalled and then REGRESSED a real barrier.
FIX: `enum cnxman_envelope_kind` {RESPONSE, NOTIFY, REQUEST} replaces the
`is_response` int at `cnxman_envelope_originate()`. Only a REQUEST mints, from
the CSB's own monotonic counter (nonzero, wrap skips 0), which is this node's
own bookkeeping exactly like send-msg# -- not a reproduction of VMS's
derivation, which sec 4(j) records as UNKNOWN and Rule 8 forbids recomputing.
A new dialogue takes a new transaction id and restarts the token. Each of the
twelve call sites now states what its body is, and each is cited to its own
census population. `vms_cm_notification_zero_txn()` corrected to zero BOTH
cells: its note claimed the token was unconstrained; the wire says 125/125 and
1104/1104 carry zero.
GATE HOLE CLOSED. `cm_wire_safety_audit.py` reported 0 FATAL / 0 WARN on the
run that killed the coordinator. Two classes added -- S13-CLOSE-STATE-ZERO and
S14-REQUEST-PAIR-ZERO -- which flag frames 894 and 888 and NOTHING else on that
capture, with ZERO false positives across 101 reference-node sources in the
47-capture library. (E82's emit guard is untouched.)
TEETH, both proven by reverting each fix: without the mint, the new test
reports 12 zero pairs and 66 token collisions -- the live-wire condition
exactly; without the refusal, the close goes out and the mid-barrier
"emits NOTHING" assertion fails.
TESTS: cluster_host+cluster_sim 64/64, full ctest 240/240, unit 113/113,
wire_safety+emit_guard 2/2, includes-gate OK (75 files), wire-safety self-test
13/13 vectors with a clean clean-fixture, Linux vms.ko builds, NetBSD-VAX
`build-vms-module-vax.sh` ALL PROOFS PASSED (elf32-vax, ILP32-clean).
New: `test_e85_barrier_survives_to_member` drives the whole 12-step barrier
with the killing close arriving mid-barrier and asserts the barrier still
reaches op-0x0c #12 -> MEMBER, that no connect and no disconnect happens across
any of it, that MEMBER follows the real #12 and nothing synthetic, and that the
twelve steps carry twelve distinct nonzero pairs.
`test_correlation_pair_is_maintained` pins the REQUEST/RESPONSE/NOTIFY rule,
the per-dialogue restart and the nonzero wrap.
ESCALATION (Fable): body[24:26] of the cat-0x86 close is MANDATORY, its
meaning is unpublished, no peer assigns it on the wire, and this executive
holds nothing that derives it -- so OVMX now answers no close at all, while all
1308 real requests in the corpus are answered. Withholding is the safer of two
unprecedented behaviours (sec 4(p) gates the barrier on the cat-0x02 op-0x0d
rebuild records, not on this close), but it IS unprecedented and wants a
ruling: ground the field, or accept the silence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl
added a commit
that referenced
this pull request
Sep 10, 2026
… of the
same nodemap decode (ROUND-ROBIN CSV SLOT, settled)
Decode only; no code changed. Two captures re-decoded with a script that uses
no project code.
=== 1. The cn3 note's premise was scoped wrong, and the error mattered ===
cn3-achieved-20260905.md says "no cat 0x01 op=0x0c (barrier release) frame ever
appears". Whole-capture census: cn3 contains 01/0b x13 and **01/0c x12**. They
belong to a SECOND transition the note does not mention -- the epoch-6
class-0x03 REMOVE at t+1584.23s, VAX1 coordinating and VAX2 participating after
OVMXJ1 departed -- which walks the canonical barrier in full: 01/0b step N ->
81/0b ack -> 01/0c release N, N = 1..12, step index in body[16:20], no gaps.
op06-join shows the same 12-step walk for a class-0x02 ADD (frames 450-1107).
The SCOPED claim -- nothing toward OVMXJ1 -- stands, and is corrected in place.
=== 2. What actually commits the membership: PHASE 2 AT THE GO ===
The cn3 epoch-5 ADD really did stall its barrier: nodemap {1,2,3} -> M=3 -> 24
op-0b expected, exactly ONE observed (OVMXJ1's step 1, acked at frame 856, never
released). VAX1 sent no op-0b for epoch 5, and that is not a capture blind spot
-- the whole epoch-6 barrier between VAX1 and VAX2 is in the same file.
The membership committed anyway: VAX1 took the GO at frame 850 (t+18.9821s) and
counted CN_3 from then, sustained to t+600s, then ran a clean epoch-6 REMOVE 26
minutes later. That is p. 7-42 exactly -- Phase 2 (nodemap into the CSBs,
quorum, the count, the CLUSTER flag) runs at the GO; the 12-step barrier is the
lock-rebuild synchronisation that FOLLOWS the commit.
VERDICT: OVMX neither over- nor under-models this. barrier_h_go() already calls
barrier_commit_phase2() ("the count commits HERE ... p. 7-42") and
phase2_commit_local_membership() sets cl->state = MEMBER there. Only the join
FSM's OWN promotion waits for op-0c #12 (E79's deliberate choice) -- a narrower
thing than "am I a member", worth a decision but not a silent change.
NO CODE FIX WARRANTED from this decode.
E85's hypothesis that the barrier gates on cat-02 op-0d rebuild records is
REFUTED: cn3 has zero 02/0d frames in the entire capture, yet its epoch-6
barrier walked all 12 steps. Why VAX1 skipped the epoch-5 barrier is genuinely
AMBIGUOUS on the wire; the run that would settle it is a 3-VAX ADD with no OVMX
present (24 steps => OVMX implicated; early stop => M>=3 ADDs differ).
=== 3. vms-3a7c IS SETTLED -- by the same nodemap, by causality ===
The answer was never in the op-06 burst; it is in the transition-open NODEMAP,
whose bits ARE the CSV slots:
epoch 5 frame 273 VAX2->VAX1 op-09 ADD body[55]=0x0e = {1,2,3}
epoch 5 frame 834 VAX2->OVMXJ1 op-09 ADD body[55]=0x0e = {1,2,3}
epoch 6 frame 11347 VAX1->VAX2 op-08 REMOVE body[55]=0x06 = {1,2}
Three stations ever on that segment (1025, 1026, 1986). Bit 3 is present in
EXACTLY the transition that admits OVMXJ1 and absent in EXACTLY the one that
removes it; no other station enters or leaves. So slot 3 is OVMXJ1's -- and
OVMXJ1's SCSSYSTEMID is 1986, whose & 0x3ff is 962, a bit index the nodemap byte
cannot even express.
=> real VMS assigns the ROUND-ROBIN CSV SLOT, not SCSSYSTEMID & 0x3ff.
Corroborated independently in op06-join (different cluster incarnation, only
1025 and 1026 on the wire): its ADD open carries nodemap 0x0a = {1,3}, and both
bits must be real members (VAX1 coordinates, VAX2 walks the 12 steps), so one of
1025/1026 holds a slot neither 1025&0x3ff=1 nor 1026&0x3ff=2 can produce.
CONSEQUENCES:
* OVMX's COORDINATOR (coord_next_slot round-robin) was already right -- no
change, and the vms-3a7c ambiguity gate can eventually be retired rather
than widened.
* OVMX's JOINER self-derive is now PROVEN WRONG: for OVMXJ1 it computes
0x000103C2 while the cluster assigned 0x00010003. With low word 962,
phase2_csb_in_nodemap() finds the slot inexpressible, answers "unknown", and
OVMX can never select itself into the map -- which is exactly why OVMX's own
executive could not have read MEMBER in the CN=3 run even while VAX1's
SHOW CLUSTER counted it. A real, grounded defect.
* But the decode does NOT say HOW a joiner should learn its slot (the nodemap
names slots, not systems). ESCALATED, not implemented.
=== 4. Two of my own earlier claims were WRONG; corrected in the note ===
(a) "0x00010003 belongs to a system absent from the segment" -- FALSE; in
op06-join slot 3 is held by one of the two VAXes that are present.
(b) "A burst never carries its recipient's own CSID" -- FALSE, and it was the
load-bearing step of the old sec 8 argument. op06-join's burst does carry
it. cn3's burst lacks OVMXJ1's record for a TEMPORAL reason: frames
245-833 precede the op-09 (834) that admitted it.
Host ctest still 247/247 (nothing built changed).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKRFgtQkJPTJg7Gg5QyLcN
baron-3dl
added a commit
that referenced
this pull request
Sep 10, 2026
… assigned
CSID, and SYMMETRIC CN=2 lands
RIG-A-FINAL role=founder member=1 state=MEMBER csid=0x00010001 cn=2 epoch=2 projections=agree
RIG-B-FINAL role=joiner member=1 state=MEMBER csid=0x00010002 cn=2 epoch=2 projections=agree
GENESIS 2-NODE PROOF PASSED
Node B now COUNTS TWO. Its own CSB table names both systems (1026 ->
0x00010002 self, 1025 -> 0x00010001 peer, both MEMBER) and its $GETSYI reads
member=1 nodes=2 -- three projections that agree, all read back out of the
executive (INV-6).
GROUNDING FIRST. The claimed op-0x05 layout was verified against every real
op-0x05 frame in both repository captures BEFORE anything was built on it --
5 in cn3-achieved-20260905.pcap and 3 in op06-join-20260903.pcap, 8/8 agreeing:
body[16:20] the constant 0x00000220, body[20:24] SCSSYSTEMID, body[28:36] boot
time, body[36:40] assigned CSID, body[40:42] index == (CSID & 0xffff) - 1.
The pairings independently re-confirm the round-robin rule (cn3: 1025->slot 1,
1026->slot 2, 1986->slot 3 although 1986 & 0x3ff = 962; op06-join: 1026->slot 3
although 1026 & 0x3ff = 2), and the distribution is on the wire too: cn3 frames
230-233 carry the FULL set to the joiner, frame 235 carries only the DELTA to
the present member.
(a) COORDINATOR. vms_cm_membership_rec_build/_parse -- one shared form, one
shared self-consistency predicate applied by builder and parser alike, so
what OVMX emits is by construction what OVMX accepts. Only grounded fields;
body[42:132] (uninterpreted stale buffer in the reference -- leftover
strings and VAX kernel pointers) is emitted ZERO and COUNTED, never
reproduced (Rule 8). coord_send_membership_set() distributes them the
reference's way. EXECUTIVE-BACKED: every field is read off the CSB the
record is about -- the SCSSYSTEMID the port learned, the CSID
coord_assign_slot() actually stamped, that CSID's own slot, that member's
real incarnation. A member this node holds no complete identity for gets NO
record (counted); a record that does not hold together is refused by the
codec and never becomes a frame.
(b) JOINER. join_adopt_membership_rec() adopts the CSID from the record naming
its own SCSSYSTEMID -- re-adopting on every admission, caching nothing
(p. 7-25: a rejoining system gets a NEW CSID; the oracle measured one
SCSSYSTEMID taking slot 4 then slot 5). Records about OTHER members are
filed on the block this CLUB already holds for that SCSSYSTEMID -- which is
precisely what lets a joiner COUNT the cluster -- and a record about a
system it holds no block for is counted and dropped, never invented.
The generation << 16 | (SCSSYSTEMID & 0x3ff) SELF-DERIVE IS GONE: it gave
OVMXJ1 low word 962 where the cluster had assigned slot 3, so
phase2_csb_in_nodemap() could never match it and OVMX could never select
itself into a cluster that had really admitted it.
(c) COMMIT AT THE GO (vms-9c99). The barrier gains phase2_commits, moved by
barrier_commit_phase2(), and the join FSM promotes on that -- the same
criterion membership and the coordinator already use. Grounded: cn3 shows a
real VAX committing an ADD and counting its joiner from the GO for 600 s
with no on-wire op-0x0c to that joiner at all, so the old op-0x0c #12
trigger was an interop hang.
ALSO REMOVED: the interim vms-3a7c "ambiguity gate" I added earlier, which
admitted a system only when the round-robin slot and SCSSYSTEMID & 0x3ff
agreed. With the rule settled and the joiner adopting rather than deriving it
is unnecessary -- and it was actively WRONG: the oracle's own behaviour
(1986 -> slot 3) is exactly a case it would have refused. op-0x05 is renamed
from VMS_CM_OP_LOCKRB to VMS_CM_OP_MEMBREC so no reader can hold the old
"lock/resource rebuild" meaning.
(d) MIRRORS: none required -- no new TU, no new ioctl, nothing crossing the shr
vector or vms_lock_nb.h. Verified.
CONTROLS, both run on the same image:
noderive node B's SCSSYSTEMID is 1030 (low ten bits = 6) and its executive
reports CSID 0x00010002 -- CSV slot 2, the slot the coordinator
ASSIGNED. A value it could not have computed, so the identity was
demonstrably adopted off the wire. HELD.
negctl VOTES=0 on both: neither founds, neither reaches MEMBER, neither
holds a CSID. HELD.
NOT CLAIMED, recorded rather than hidden: node B's quorum arithmetic is still
empty (quorum=0 cevotes=0) -- it counts both members but has not learned node
A's VOTES, which ride op-0x01 PARAMS, not the membership record. And node A
logs two "unroutable VMS$VAXcluster frame" lines: node B's 0x81/0x05 echoes,
which A's coordinator owns no edge for and says so rather than inventing one.
TESTS. Host ctest 250/250 (serial; opcom_record_body_gate times out only under
-j parallel load and passes standalone in 9.9 s -- pre-existing, unrelated).
New: test_membership_rec (builder/parser round-trip on the captures' own
vectors, the refusals, body[42:132] proven zero),
test_membership_records_are_projected_from_the_csbs (full-set/delta
distribution, and every emitted field compared against the CSB it is about),
test_slot_is_assigned_not_derivable, and four joiner cases -- adopt-when-named,
not-adopted-when-about-another (but filed on that peer), re-adopt-on-rejoin,
unusable-record-answered-not-adopted. The op-0x06 self-derive assertions were
re-pointed at the corrected behaviour, and the E79 promotion test now pins the
GO. cluster_core_includes_gate and cluster_wire_safety_gate pass; elf32-vax
cross-compiles + relocatable-links ILP32-clean; the Linux vms.ko is what the
rig boots.
Lab torn down (ovmx-genesis-rig + fetch pod deleted); vaxlab-* untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKRFgtQkJPTJg7Gg5QyLcN
baron-3dl
added a commit
that referenced
this pull request
Sep 10, 2026
…full membership (op-05 adopt, commit-at-GO) (#1119) * vms-f6b: the hermetic 2-node cluster GENESIS rig -- and what it measured THE RIG. Two minimal Linux guests, each loading the REAL executive (vms.ko) and issuing STARTUP.EXE's own two cluster ioctls -- VMS_IOCTL_SYSGEN_LOAD then VMS_IOCTL_CLUSTER_START -- on one L2 segment carried by a QEMU socket netdev inside a single pod's netns (no bridge, no NET_ADMIN, no NET_RAW; only /dev/kvm). There is NO userspace SCS daemon: CLUSTER_START opens the LAN port INSIDE the executive and every 0x6007 frame on the segment is the executive's. The guest-side `cluster_node` issues ioctls and READS STATE BACK; it never touches the wire, never parses a frame and never computes a CSID, a member count or a quorum. Every printed value is a field read from the executive one line earlier through three independent projections (CLUSTER_DIAG_CSB's CLUB, CLUSTER_GETSYI, CLUSTER_MEMBER_GET), and the verdict prints whether they AGREE rather than picking one (INV-6). A value the executive has not learned prints "-", never 0. THE LIGHT PATH WORKS. No distro boot, no ODS-2 system disk, no STARTUP.EXE: insmod + REGISTER + SYSGEN_LOAD + CLUSTER_START is enough to bring PEA0:, SCS and the connection manager up on the guest's virtio NIC. WHAT IT MEASURED -- GENESIS HOLDS AT RUNTIME, on a real executive: RIG-A-FINAL role=founder member=1 state=MEMBER csid=0x00010001 cn=1 quorum=1 cevotes=1 epoch=1 projections=agree Node A (VOTES=1, EXPECTED_VOTES=1, VAXCLUSTER=2) spent its whole RECNXINTERVAL discovery window hearing nobody and formed generation 1 -- "%CNXMAN, this node has quorum by its own votes: forming an OpenVMS Cluster" -> "this node is a member of the cluster" -> "completed VAXcluster state transition" -- and its executive reports MEMBER with the minted CSID and epoch 1. NEGATIVE CONTROL (same image, same pod, VOTES=0 on BOTH nodes): neither node founds, neither reaches MEMBER, neither holds a CSID. The control is stronger than "CN != 2" deliberately, because genesis is what this rung actually measures. Both transcripts are committed under tests/qemu/captures/. WHAT IT DID NOT REACH, HONESTLY: CN=2. Node B stays JOINING with no CSID, so the rig FAILS its own verdict and says so. The join now runs deep -- both nodes open the VMS$VAXcluster VC and EMIT their cat-0x01 op-0x02 membership request (RIG-*-JOINREC kind=2 cat=0x01 op=0x02) -- but neither node's coordinator is ever driven by the peer's request, so admission times out on both sides. Beyond that sits a second, structural wall: OVMX has no op-0x06 MEMBERSHIP builder (its payload map is not grounded -- Rule 8), and op-0x06 is the ONLY path by which a joiner learns the generation it needs for its own CSID, which phase 2 needs to set the local CSB's MEMBER flag. Both are named in the report, not worked around here. TWO REAL DEFECTS THE RIG FOUND, AND FIXED: 1. vms_lan_rx_thunk() linearized a SHARED skb -- src/kernel/ exec_kbackend_linux.h. A packet_type handler is not the exclusive owner of its skb: with anything else listening on the same NIC it arrives via deliver_skb() with an extra reference, and pskb_expand_head() asserts BUG_ON(skb_shared(skb)). MEASURED: with a passive AF_PACKET capture bound to the same interface BOTH guests took a kernel panic ("kernel BUG at net/core/skbuff.c:2138 ... vms_lan_rx_thunk [vms]") on the FIRST 0x6007 frame -- i.e. tcpdump on a clustered OVMX node crashed that node. Fixed with skb_share_check(), the contract every in-tree ptype handler observes. Proven by the same rig: 150 s clean with the capture bound, and an 81 KB pcap per node where there used to be a panic. 2. cnxman_join_drive() downgraded a MEMBER to JOINING -- src/kernel-core/ vms_cnxman.c. The beat drives a join whenever a system is present and the join FSM is idle, INCLUDING on a node that is already a member (either side may open the VMS$VAXcluster connection, E67). The unconditional `cl->state = VMS_CLUSTER_JOINING` therefore un-asserted a membership the executive genuinely held: the local CSB still carried MEMBER and the CLUB still carried the CSID, while the one cell SYI$_CLUSTER_MEMBER and SHOW CLUSTER read said JOINING. MEASURED: node A founded the cluster, was committed MEMBER by phase 2, and reported member=0 the instant node B was powered on -- a cluster's founder and coordinator describing itself as not a member of it. Only phase 2 may write MEMBER and only a real transition may take it away; a join ATTEMPT is neither. TWO RIG-LEVEL FINDINGS RECORDED IN THE CODE: - CLUSTER_CREDITS is load-bearing. With it unset the port advertises a grant of 0 at abs 95, the peer honours "no credit, no message" (p. 2-43) and the circuit opens with NOTHING able to travel on it: measured as both VCs OPEN, credits_send=0, zero sequenced frames either way. The executive's behaviour was exactly right (it counts vc_credits_absent and refuses); the rig was misconfigured. It now loads 32, VMS's own default. - QEMU's socket,mcast= netdev loops a guest's own group multicast back to it, and a node booted ALONE formed a channel with ITSELF off its own HELLO ("%PEA0, channel verified" with no peer on the segment). The rig uses a point-to-point socket instead. That the port accepts a HELLO bearing its own SCSSYSTEMID is a separate robustness observation, recorded not patched. TESTS RUN. Full host ctest 247/247 pass, including a new source scan in test_cnxman_glue.c pinning fix 2 (the guarded write present, the unguarded one gone). cluster_core_includes_gate and cluster_wire_safety_gate pass. The Linux vms.ko builds clean (it is what the rig boots) and the whole executive module cross-compiles + relocatable-links for elf32-vax against real NetBSD/vax kernel headers, ILP32 width-clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKRFgtQkJPTJg7Gg5QyLcN * vms-f6b/vms-1ee: the op-0x06 MEMBERSHIP builder -- an OVMX coordinator can admit a joiner, and the joiner reaches MEMBER on a wire-learned CSID DELIVERABLE 1 -- docs/design-op06-membership-builder.md. Every field of the cat-0x01 op-0x06 record, grounded or honestly omitted, re-decoded from the captures by a script that uses NO project code: primary tests/lab/captures/op06-join-20260903.pcap (255 real op-0x06 frames, VAX1 coordinator) cross-referenced against cn3-achieved-20260905.pcap (254 frames, VAX2 coordinator, a DIFFERENT cluster and epoch -- and the burst OVMX itself consumed to reach CN=3), with scs-tier0 grounding SCSSYSTEMID inside the cluster-LOGICAL LAN address (aa:00:04:00:01:04 -> 0x0401 = 1025). The whole 24-byte CM header is byte-identical across all 509 frames: category 0x01, opcode 0x06, role 0x20 (COMMIT), class 0x02 (ADD), body[18:20] zero -- and body[12:16] the sender's real transition epoch (6 in one capture, 5 in the other). Form A's CSID at body[24:28] carries ONLY genuine CSIDs, 24/24 in each capture, zero false positives. THE :715 QUESTION, ANSWERED BY MEASUREMENT. That note forbids a zero-filled op-0x06 on the premise that the burst carries a membership LIST, so zeros would assert an empty cluster. E30 falsified that premise and the captures refute it: 23 of 255 real op-0x06 frames -- and 23 of 254 in the second capture -- carry an ENTIRELY ZERO body[24:132], interleaved through the burst (first at position 5 of 255, then every ~11 frames), in joins that SUCCEEDED. A zero payload is a shape the reference emits as a matter of course. The prohibition still stands for the two opcodes that note also names, and neither gains a builder here. DELIVERABLE 2 -- vms_cm_membership_build(). ONE frame per admission (the reference's 254-frame burst is E78's crash vector), carrying the coordinator's OWN real CSID read from club->local_csid at the form-A offset -- the offset the receive path tries FIRST, so builder and reader share one form, enforced by a round-trip test. A value failing the SAME shape test the reader applies is VMS_CODEC_E_RANGE and no frame is built. Seven nonzero bytes in the whole 132, asserted as such; the countdown, the incarnation and the sub-record body are left zero and COUNTED (membership_fields_omitted). MEASURED ON TWO REAL EXECUTIVES (rig on the k3s worker, KVM; every value read back through CLUSTER_DIAG_CSB / _GETSYI / _MEMBER_GET): RIG-A-FINAL role=founder member=1 MEMBER csid=0x00010001 cn=2 epoch=2 RIG-B-FINAL role=member-no-coordinator member=1 MEMBER csid=0x00010002 cn=1 epoch=2 node A: %CNXMAN, proposing addition of a system to the cluster %CNXMAN, system 0000000000000402 was added to the cluster node B: %CNXMAN, this node is a member of the cluster Node B did not receive that CSID -- it read a GENERATION out of node A's op-0x06 and computed 1<<16 | (1026 & 0x3ff) from its own SYSGEN state, and phase 2 committed its membership off the nodemap bit node A really asserted. TWO EXECUTIVE DEFECTS THE RIG FOUND ON THE WAY, both blocking any admission: 1. THE JOINER'S ADVERT HANDLER SWALLOWED THE op-0x02 MEMBERSHIP REQUEST. join_h_peer_advert() consumed cat-0x01 op-0x02 as a "peer advert", and the router offers a body to the join FSM FIRST -- so the coordinator's ONE selection edge ([IDLE][RX_TR_REQUEST]; being asked is what MAKES a node the coordinator, book pp. 7-37/7-38) was never reached. Measured: both nodes put their op-0x02 on the wire for a whole run and neither ever proposed anything. It now returns NOT_MINE for that one opcode -- op-0x01 is handled there and op-0x14 has no other owner, so neither becomes an unroutable frame. 2. Two silent refusal paths in the coordinator now say what they did once. A transition that stalls in COMMIT used to leave no trace at all: the counters are projected through no ioctl and the executive has no console log of its own. CSID ASSIGNMENT IS PROVISIONAL PENDING ORACLE (vms-3a7c), and is GATED rather than kill-switched. coord_csid_unambiguous(): admit a subject only when the round-robin CSV slot and the joiner's own SCSSYSTEMID & 0x3ff -- the two candidate rules -- name it IDENTICALLY. Under that gate the CSID OVMX asserts is correct whichever way the oracle settles, which is the only "safe toward a real VAX" available before the answer exists. A disagreement is a named refusal with nothing stamped and nothing emitted. The note lays out both candidate rules, what the captures ground (every real CSID is consistent with BOTH -- the lab's SCSSYSTEMIDs are consecutive from 1025, so no capture can separate them) and the single lab run that would. THREE RIG MODES, all three RUN: proof A founds, admits B; both MEMBER; A cn=2, B cn=1 (see below) -- the rig FAILS its own strict verdict and says precisely what held. ambig node B's SCSSYSTEMID 1030 makes the two rules disagree: node A refuses ("cannot be assigned unambiguously"), sends NO op-0x06, and node B stays role=none member=0 csid=- JOINING. HELD. This is the INV-6 control on the joiner -- same code, same wire, one SYSGEN digit apart. negctl VOTES=0 on both: nobody founds, nobody reaches MEMBER, nobody holds a CSID. HELD. THE REMAINING GAP, STATED NOT PAPERED OVER (note sec 8). Node B counts 1 member, not 2: phase 2 can only match a CSB to a nodemap bit if that CSB's CSID is known, and no grounded wire field associates a PEER's SCSSYSTEMID with its CSID. B's own executive says so -- "%CNXMAN, committed member count differs from the transition nodemap". B cannot take the CSID off the op-0x06 it just read, because E30 and the captures are explicit that a burst carries the sender's own CSID OR another member's: in cn3 the sender is VAX2 (1026) and its form-A record carries VAX1's 0x00010001. Attributing it to the sender is a fabrication the capture refutes, so it is NOT done. Two candidate closures that would work inside an OVMX-coordinated cluster but assert the unresolved vms-3a7c rule about a system this node was never told about are written down and ESCALATED, not implemented; the clean answer is one lab capture, which would settle vms-3a7c in the same run. TESTS RUN. Host ctest 247/247. New: test_membership_build (builder bytes, refusal, round-trip through this codec's own reader, every-nonzero-byte count), test_membership_record_is_built_from_real_state (the emitted frame's grounded bytes and its zeros, against real CLUB state) and test_ambiguous_csid_assignment_refuses. test_cnxman_coord's bed now carries REAL SCSSYSTEMIDs consistent with its own CSID constants (1025/1026/1027/1028) rather than LAN-address-shaped 48-bit values for which slot != sysid & 0x3ff -- a fixture made faithful, with every assertion unchanged. cluster_core_includes_gate and cluster_wire_safety_gate pass. Linux vms.ko builds (it is what the rig boots) and the whole executive module cross-compiles + relocatable-links for elf32-vax against real NetBSD/vax headers, ILP32 width-clean. NO REAL-VAX EMISSION. The rig is all-OVMX and emits toward no VAX. Nothing here is claimed interop-verified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKRFgtQkJPTJg7Gg5QyLcN * vms-3a7c: cn3 does NOT settle the CSID-assignment rule -- decoded, and why The hypothesis was attractive: cn3-achieved-20260905.pcap admits OVMXJ1, whose SCSSYSTEMID 1986 is non-consecutive (1986 & 0x3ff = 962 = 0x3C2 vs next free CSV slot 3), so if the coordinator's assignment for it were visible the two rival rules would predict 0x00010003 vs 0x000103C2 and vms-3a7c would be over from existing data. Decoded. It is not visible, for three independent reasons, each measured: 1. NO PRE-JOIN BASELINE. cn3 contains ZERO cat-0x01 op-0x06 frames before OVMXJ1 first speaks (t+15.247s). All 254 are VAX2 -> OVMXJ1 and begin at t+18.933s BECAUSE of the join. All three CSIDs (0x00010001/2/3) first appear within ONE MILLISECOND of each other in the burst's opening frames, so "a third CSID appears when the new member arrives" is unobservable -- every CSID appears then. The temporal discriminator has nothing to discriminate against. 2. 0x00010003 IS NOT OVMXJ1's, AND THE SIBLING CAPTURE PROVES IT. op06-join-20260903.pcap has only TWO stations on the wire -- 1025 and 1026, no third VAX at all -- and its burst nonetheless asserts 0x00010003 x46 at the grounded form-B offset, the IDENTICAL multiplicity cn3 shows. That record therefore belongs to a member of the coordinator's own table that is ABSENT from the segment (p. 7-25's retained CSB), and cn3 re-asserts the same foreign record. The elimination argument ("three stations, three CSIDs, so the third is the joiner's") fails on this. 3. A BURST NEVER CARRIES ITS RECIPIENT'S OWN CSID -- E30 from a second angle: VAX1 -> VAX2 : 0x00010001 x47, 0x00010003 x46 (0x00010002 ABSENT) VAX2 -> OVMXJ1 : 0x00010001 x47, 0x00010002 x23, 0x00010003 x46 (OVMXJ1's ABSENT) So OVMXJ1's assigned CSID is not in cn3 at all, under either rule. The negative check is inconclusive, not confirmatory: 0x000103C2 occurs 0 times in all 11,478 SCA frames of cn3 -- but since the burst structurally omits the recipient's own CSID, that absence is exactly what BOTH rules predict. (A bare value-scan for 0x00010003 across all frames finds 302 hits, but 242 sit at abs 82 = body[10], the uninitialised-residue span, and the first is 6.8s BEFORE OVMXJ1 exists -- which is its own warning against attributing by value.) WHAT THIS BUYS ANYWAY: the required lab capture is now sharply specified rather than merely named. It must (a) capture the coordinator's burst toward an ALREADY-PRESENT member, never toward the joiner -- point 3 is why -- and (b) include a PRE-ADMISSION baseline, which cn3 lacks entirely. With the new node's SCSSYSTEMID & 0x3ff chosen NOT to equal the next free CSV slot, that one run settles vms-3a7c AND grounds the {SCSSYSTEMID -> CSID} association sec 8 needs for symmetric CN=2. Note-only; no code changed. The provisional round-robin rule and the sec 5 ambiguity gate stand exactly as they were. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKRFgtQkJPTJg7Gg5QyLcN * vms-fc7: ground the transition-commit model -- and vms-3a7c falls out of the same nodemap decode (ROUND-ROBIN CSV SLOT, settled) Decode only; no code changed. Two captures re-decoded with a script that uses no project code. === 1. The cn3 note's premise was scoped wrong, and the error mattered === cn3-achieved-20260905.md says "no cat 0x01 op=0x0c (barrier release) frame ever appears". Whole-capture census: cn3 contains 01/0b x13 and **01/0c x12**. They belong to a SECOND transition the note does not mention -- the epoch-6 class-0x03 REMOVE at t+1584.23s, VAX1 coordinating and VAX2 participating after OVMXJ1 departed -- which walks the canonical barrier in full: 01/0b step N -> 81/0b ack -> 01/0c release N, N = 1..12, step index in body[16:20], no gaps. op06-join shows the same 12-step walk for a class-0x02 ADD (frames 450-1107). The SCOPED claim -- nothing toward OVMXJ1 -- stands, and is corrected in place. === 2. What actually commits the membership: PHASE 2 AT THE GO === The cn3 epoch-5 ADD really did stall its barrier: nodemap {1,2,3} -> M=3 -> 24 op-0b expected, exactly ONE observed (OVMXJ1's step 1, acked at frame 856, never released). VAX1 sent no op-0b for epoch 5, and that is not a capture blind spot -- the whole epoch-6 barrier between VAX1 and VAX2 is in the same file. The membership committed anyway: VAX1 took the GO at frame 850 (t+18.9821s) and counted CN_3 from then, sustained to t+600s, then ran a clean epoch-6 REMOVE 26 minutes later. That is p. 7-42 exactly -- Phase 2 (nodemap into the CSBs, quorum, the count, the CLUSTER flag) runs at the GO; the 12-step barrier is the lock-rebuild synchronisation that FOLLOWS the commit. VERDICT: OVMX neither over- nor under-models this. barrier_h_go() already calls barrier_commit_phase2() ("the count commits HERE ... p. 7-42") and phase2_commit_local_membership() sets cl->state = MEMBER there. Only the join FSM's OWN promotion waits for op-0c #12 (E79's deliberate choice) -- a narrower thing than "am I a member", worth a decision but not a silent change. NO CODE FIX WARRANTED from this decode. E85's hypothesis that the barrier gates on cat-02 op-0d rebuild records is REFUTED: cn3 has zero 02/0d frames in the entire capture, yet its epoch-6 barrier walked all 12 steps. Why VAX1 skipped the epoch-5 barrier is genuinely AMBIGUOUS on the wire; the run that would settle it is a 3-VAX ADD with no OVMX present (24 steps => OVMX implicated; early stop => M>=3 ADDs differ). === 3. vms-3a7c IS SETTLED -- by the same nodemap, by causality === The answer was never in the op-06 burst; it is in the transition-open NODEMAP, whose bits ARE the CSV slots: epoch 5 frame 273 VAX2->VAX1 op-09 ADD body[55]=0x0e = {1,2,3} epoch 5 frame 834 VAX2->OVMXJ1 op-09 ADD body[55]=0x0e = {1,2,3} epoch 6 frame 11347 VAX1->VAX2 op-08 REMOVE body[55]=0x06 = {1,2} Three stations ever on that segment (1025, 1026, 1986). Bit 3 is present in EXACTLY the transition that admits OVMXJ1 and absent in EXACTLY the one that removes it; no other station enters or leaves. So slot 3 is OVMXJ1's -- and OVMXJ1's SCSSYSTEMID is 1986, whose & 0x3ff is 962, a bit index the nodemap byte cannot even express. => real VMS assigns the ROUND-ROBIN CSV SLOT, not SCSSYSTEMID & 0x3ff. Corroborated independently in op06-join (different cluster incarnation, only 1025 and 1026 on the wire): its ADD open carries nodemap 0x0a = {1,3}, and both bits must be real members (VAX1 coordinates, VAX2 walks the 12 steps), so one of 1025/1026 holds a slot neither 1025&0x3ff=1 nor 1026&0x3ff=2 can produce. CONSEQUENCES: * OVMX's COORDINATOR (coord_next_slot round-robin) was already right -- no change, and the vms-3a7c ambiguity gate can eventually be retired rather than widened. * OVMX's JOINER self-derive is now PROVEN WRONG: for OVMXJ1 it computes 0x000103C2 while the cluster assigned 0x00010003. With low word 962, phase2_csb_in_nodemap() finds the slot inexpressible, answers "unknown", and OVMX can never select itself into the map -- which is exactly why OVMX's own executive could not have read MEMBER in the CN=3 run even while VAX1's SHOW CLUSTER counted it. A real, grounded defect. * But the decode does NOT say HOW a joiner should learn its slot (the nodemap names slots, not systems). ESCALATED, not implemented. === 4. Two of my own earlier claims were WRONG; corrected in the note === (a) "0x00010003 belongs to a system absent from the segment" -- FALSE; in op06-join slot 3 is held by one of the two VAXes that are present. (b) "A burst never carries its recipient's own CSID" -- FALSE, and it was the load-bearing step of the old sec 8 argument. op06-join's burst does carry it. cn3's burst lacks OVMXJ1's record for a TEMPORAL reason: frames 245-833 precede the op-09 (834) that admitted it. Host ctest still 247/247 (nothing built changed). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKRFgtQkJPTJg7Gg5QyLcN * vms-fc7/vms-9c99: op-0x05 membership records -- the joiner ADOPTS its assigned CSID, and SYMMETRIC CN=2 lands RIG-A-FINAL role=founder member=1 state=MEMBER csid=0x00010001 cn=2 epoch=2 projections=agree RIG-B-FINAL role=joiner member=1 state=MEMBER csid=0x00010002 cn=2 epoch=2 projections=agree GENESIS 2-NODE PROOF PASSED Node B now COUNTS TWO. Its own CSB table names both systems (1026 -> 0x00010002 self, 1025 -> 0x00010001 peer, both MEMBER) and its $GETSYI reads member=1 nodes=2 -- three projections that agree, all read back out of the executive (INV-6). GROUNDING FIRST. The claimed op-0x05 layout was verified against every real op-0x05 frame in both repository captures BEFORE anything was built on it -- 5 in cn3-achieved-20260905.pcap and 3 in op06-join-20260903.pcap, 8/8 agreeing: body[16:20] the constant 0x00000220, body[20:24] SCSSYSTEMID, body[28:36] boot time, body[36:40] assigned CSID, body[40:42] index == (CSID & 0xffff) - 1. The pairings independently re-confirm the round-robin rule (cn3: 1025->slot 1, 1026->slot 2, 1986->slot 3 although 1986 & 0x3ff = 962; op06-join: 1026->slot 3 although 1026 & 0x3ff = 2), and the distribution is on the wire too: cn3 frames 230-233 carry the FULL set to the joiner, frame 235 carries only the DELTA to the present member. (a) COORDINATOR. vms_cm_membership_rec_build/_parse -- one shared form, one shared self-consistency predicate applied by builder and parser alike, so what OVMX emits is by construction what OVMX accepts. Only grounded fields; body[42:132] (uninterpreted stale buffer in the reference -- leftover strings and VAX kernel pointers) is emitted ZERO and COUNTED, never reproduced (Rule 8). coord_send_membership_set() distributes them the reference's way. EXECUTIVE-BACKED: every field is read off the CSB the record is about -- the SCSSYSTEMID the port learned, the CSID coord_assign_slot() actually stamped, that CSID's own slot, that member's real incarnation. A member this node holds no complete identity for gets NO record (counted); a record that does not hold together is refused by the codec and never becomes a frame. (b) JOINER. join_adopt_membership_rec() adopts the CSID from the record naming its own SCSSYSTEMID -- re-adopting on every admission, caching nothing (p. 7-25: a rejoining system gets a NEW CSID; the oracle measured one SCSSYSTEMID taking slot 4 then slot 5). Records about OTHER members are filed on the block this CLUB already holds for that SCSSYSTEMID -- which is precisely what lets a joiner COUNT the cluster -- and a record about a system it holds no block for is counted and dropped, never invented. The generation << 16 | (SCSSYSTEMID & 0x3ff) SELF-DERIVE IS GONE: it gave OVMXJ1 low word 962 where the cluster had assigned slot 3, so phase2_csb_in_nodemap() could never match it and OVMX could never select itself into a cluster that had really admitted it. (c) COMMIT AT THE GO (vms-9c99). The barrier gains phase2_commits, moved by barrier_commit_phase2(), and the join FSM promotes on that -- the same criterion membership and the coordinator already use. Grounded: cn3 shows a real VAX committing an ADD and counting its joiner from the GO for 600 s with no on-wire op-0x0c to that joiner at all, so the old op-0x0c #12 trigger was an interop hang. ALSO REMOVED: the interim vms-3a7c "ambiguity gate" I added earlier, which admitted a system only when the round-robin slot and SCSSYSTEMID & 0x3ff agreed. With the rule settled and the joiner adopting rather than deriving it is unnecessary -- and it was actively WRONG: the oracle's own behaviour (1986 -> slot 3) is exactly a case it would have refused. op-0x05 is renamed from VMS_CM_OP_LOCKRB to VMS_CM_OP_MEMBREC so no reader can hold the old "lock/resource rebuild" meaning. (d) MIRRORS: none required -- no new TU, no new ioctl, nothing crossing the shr vector or vms_lock_nb.h. Verified. CONTROLS, both run on the same image: noderive node B's SCSSYSTEMID is 1030 (low ten bits = 6) and its executive reports CSID 0x00010002 -- CSV slot 2, the slot the coordinator ASSIGNED. A value it could not have computed, so the identity was demonstrably adopted off the wire. HELD. negctl VOTES=0 on both: neither founds, neither reaches MEMBER, neither holds a CSID. HELD. NOT CLAIMED, recorded rather than hidden: node B's quorum arithmetic is still empty (quorum=0 cevotes=0) -- it counts both members but has not learned node A's VOTES, which ride op-0x01 PARAMS, not the membership record. And node A logs two "unroutable VMS$VAXcluster frame" lines: node B's 0x81/0x05 echoes, which A's coordinator owns no edge for and says so rather than inventing one. TESTS. Host ctest 250/250 (serial; opcom_record_body_gate times out only under -j parallel load and passes standalone in 9.9 s -- pre-existing, unrelated). New: test_membership_rec (builder/parser round-trip on the captures' own vectors, the refusals, body[42:132] proven zero), test_membership_records_are_projected_from_the_csbs (full-set/delta distribution, and every emitted field compared against the CSB it is about), test_slot_is_assigned_not_derivable, and four joiner cases -- adopt-when-named, not-adopted-when-about-another (but filed on that peer), re-adopt-on-rejoin, unusable-record-answered-not-adopted. The op-0x06 self-derive assertions were re-pointed at the corrected behaviour, and the E79 promotion test now pins the GO. cluster_core_includes_gate and cluster_wire_safety_gate pass; elf32-vax cross-compiles + relocatable-links ILP32-clean; the Linux vms.ko is what the rig boots. Lab torn down (ovmx-genesis-rig + fetch pod deleted); vaxlab-* untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKRFgtQkJPTJg7Gg5QyLcN --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the first of the three known Rule 11 facades with a real executive facility. Partial by design — see What this does not do.
The bug
src/vmsprocess/vms_pcb.ckept the PCB in process-local memory, sosys$creprc'sprcnamwas discarded atexecand a VMS process name was unobservable from anywhere. Nothing could look a process up by name.A previous attempt bridged the name across
execin aVMS_PRCNAMenvironment variable, following the existingVMS_USERNAME/VMS_TERMINALconvention. It compiled, tested green, and was a cheat — a process self-reporting a name nothing else can see. It was rejected.The actual fix
The name never needed a carrier.
execve()does not change the Linux pid, andvms.koalready keysstruct vms_procbycurrent->pid— so identity survivesexecvia a lifetime change invms_dev_release()(free only onPF_EXITING, plus lazy reaping), not via userspace state. Newsrc/kernel/vms_proctab.cplus its ioctl surface and client.Proven A-writes / B-reads (Rule 11's decisive test) against a real
/dev/vmsin QEMU: 30 passed / 0 failed, 11 suites, with a negative control and cross-process resolution of a pid the reader never supplied.Oracle pinning — the tree was wrong in more places than suspected
ssdef.h:74claimedSS$_POWERFAIL 598 /* VMS: 0x254; 596 taken by SS$_IVLOGNAM */. Both halves were wrong. Pinned on the live VAX 7.3 lab by two independent documented-tool methods ($SSDEFextract +SEARCH, andF$MESSAGEround-trip) plus behavioural probes:SS$_DUPLNAM 148·SS$_IVLOGNAM 340·SS$_NOIOCHAN 434·SS$_VOLINV 596·SS$_POWERFAIL 868·SS$_NONEXPR 2280·SS$_RIGHTSFULL 2540·SS$_IVSECFLG 364·SS$_UNASEFC 564·SS$_SYNCH 1673Correcting those surfaced three value collisions in the header, all resolved.
grep | sort -n | uniq -dnow reports only the intentionalSS$_NORMAL == SS$_CONTINUE.The worse bug found while fixing a lesser one
The client truncated an oversized process name in userspace, so the executive's validator never saw it. Fixing that revealed the
$GETJPIlookup key was clipped too — an overlong name resolved the process holding its first 15 characters, i.e. answered for a different process. The key now has its ownsel_prcnamfield andinfo.prcnamis output-only. Oracle boundary: 15 chars accepted, 16 rejected withSS$_IVLOGNAM; VMS neither truncates nor partially applies.Verification
Every claim proven by execution, and independently re-derived by an adversarial reviewer from a clean
git archiverather than from the report:setgid(300)-while-uid 0helper. Deleting the group check flips exactly the three discriminators (30/0 → 25/5). Restoring userspace truncation flips exactly the five length assertions.3 passed / 8 failed;ci.yml's pinned literal moved 7 → 8, with a comment recording that the brittleness is the detector and must never be softened into a range.ctest40/40, 1 known skip. No test deleted, skipped or weakened; 8 assertions added.What this does not do
sys$getjpi,dcl_cmd_show.candsys$creprcare not yet readers of this table, soSHOW SYSTEMstill prints only the caller. That waits onvms-9fcwiringvms_kif_register()— today it has zero callers, so converting them would need either a forbidden per-process fallback or would break the host suite where/dev/vmsdoes not exist.vms-8019stays open.vms.koand userspace must be rebuilt together. The interface is new on this branch and has no released consumers. Struct sizes and ioctl encodings are now locked by_Static_assertwith a wrong-literal negative control — the substitute for an x86_64 runtime proof, which this host cannot run (vms-d4brecords the gap and its human prerequisite).🤖 Generated with Claude Code