Skip to content

vms-dca9: DLM rung H10b — a cross-node lock survives its master's departure (remaster rebuild) - #928

Merged
baron-3dl merged 3 commits into
mainfrom
work/vms-dca9
Aug 29, 2026
Merged

vms-dca9: DLM rung H10b — a cross-node lock survives its master's departure (remaster rebuild)#928
baron-3dl merged 3 commits into
mainfrom
work/vms-dca9

Conversation

@baron-3dl

Copy link
Copy Markdown
Contributor

DLM rung H10b (rd vms-dca9)

Proves the remaster invariant: a cross-node lock SURVIVES the departure of its master node. The surviving holder re-registers its lock on the NEW directory master, which rebuilds res->granted from the holder's real origin state — never a plausible default (INV-6) — and the harness value-verifies the rebuilt lock equals the one held pre-departure.

Executive (8ea2c987)

  • New DLM wire op SCS_DLM_OP_REBUILD / VMS_DLM_OP_REBUILD = 5, registered in every place the "op in N places" trap requires (scs_dlm.h + vms_ioctl.h enums, scs_dlm.c codec validator + op_name, scsd.c static_assert, vms_lock.c dispatch). Reuses the existing DLM frame (resnam + req_lkid + mode + req_csid) — an OVMX design choice (Rule 8; the DLM SYSAP wire is OVMX's own).
  • vms_lock_dlm_xnode_rebuild() RECONSTRUCTS the holder's exact prior grant directly into res->granted (req_csid=holder, req_lkid=holder's handle, granted_mode=real mode) and makes this node the master. It does not re-run the enq/grant core (which would re-derive a grant against this node's empty queue). Refuses a zero/NL rebuild (SS$_BADPARAM).
  • New readback ioctl VMS_IOCTL_DLM_GET_GRANTED (0x37, additive) surfaces the first remote-held grant's {holder_csid, holder_req_lkid, granted_mode} so the harness can value-verify (GET_RESMASTER exposes neither mode nor handle). Census-reconciled up-front: the vms_kif_dlm_get_granted wrapper is declared OVMX-UNWIRED (scsd issues it directly).

Userspace drive + harness (97a88907)

  • peer_by_csid() — first DLM sender that must reach a peer other than the one whose join tick drove the call (the resource's new master, not the departed one).
  • scsd_dlm_client_send_rebuild() — targeted SCS_DLM_OP_REBUILD, registered in the CHOKED SEND SITE TABLE (labeled new-in-vms-dca9).
  • Drive (gated OVMX_DLM_H10B): holder A, on the master's class-0x04 departure, reads its origin's REAL mode via GETLKI, re-resolves N via local GET_RESMASTER, and sends the REBUILD to N; new master B dispatches it into the executive then reads back via GET_GRANTED — every marker carries real values, sent=0 reported honestly if a read fails.
  • New 3-node h10b harness (A=1030 holder / B=1031 survivor-master / C=1032 departing master) + dlm-harness-h10b CI job.

Proof — real 3-node QEMU/KVM cluster, exit 0

SCSD-I-DLMHOLDOK name=RES_C master_csid=1032 req_lkid=1 mode=EX
SCSD-I-DLMREBUILDSENT csid=1031 lkid=0x00000001 mode=EX (getlki_rc=0x1 dir_rc=0x1 sent=1)
SCSD-I-DLMRX, cross-node REBUILD from CSID=1030 resnam='RES_C' -> executive status=0x1
SCSD-I-DLMREBUILT found=1 n_granted=1 holder_csid=1030 lkid=0x00000001 mode=EX
  DLM HARNESS H10b PASSED

Three-way value equality held: A_sent(lkid=1,mode=EX) == B_readback(lkid=1,mode=EX), B_readback holder_csid==1030(A). New master targeted was B (survivor), never C (departed).

scs_send_sites census green (113 checks); kif_caller_census green.

Observed gap (out of scope, flag for tracking)

The executive's cross-node ENQ receive path (vms_lock_dlm_xnode_dispatch) masters unconditionally whatever ENQ it receives — no receive-side directory check. Every prior DLM rung was 2-node so this never surfaced; H10b sidesteps it with an opt-in OVMX_DLM_ENQ_CSID targeting env (additive, byte-identical when absent) so the harness ENQ reaches RES_C's real master. The underlying receive-side-mastership check is worth its own item.

🤖 Generated with Claude Code

baron-3dl and others added 3 commits August 29, 2026 22:07
…5 REBUILD + GET_GRANTED)

The executive half of "a cross-node lock survives the remaster": a surviving
holder re-registers its lock on the NEW master, which reconstructs it directly
into res->granted from the holder's REAL origin state.

- New DLM wire op SCS_DLM_OP_REBUILD / VMS_DLM_OP_REBUILD = 5 (an OVMX design
  choice, Rule 8 -- the DLM SYSAP wire is OVMX's own). Registered in every place:
  the scs_dlm.h + vms_ioctl.h op enums, the scs_dlm.c codec validator +
  scs_dlm_op_name, the scsd.c static_assert pinning the two enums, and the
  vms_lock_dlm_xnode_dispatch switch. Reuses the existing frame (resnam +
  req_lkid + mode + req_csid -- no new field).
- vms_lock_dlm_xnode_rebuild(): RECONSTRUCTS the holder's exact prior grant --
  resource_find_or_create, a lock entry stamped req_csid=holder, req_lkid=the
  holder's own handle, granted_mode=the mode it really held, added DIRECTLY to
  res->granted (like the DEQ handler manipulates the queues directly), and this
  node becomes the master. It does NOT re-run the enq/grant core, which would
  re-derive a grant against this (new) node's empty queue rather than preserve
  the holder's real lock. INV-6: the values are the holder's REAL origin state
  transported over SCS; a zero/NL rebuild is refused (SS$_BADPARAM).
- New readback ioctl VMS_IOCTL_DLM_GET_GRANTED (0x37, additive): reports the
  first remote-held granted lock's {holder_csid, holder_req_lkid, granted_mode}
  so the harness can VALUE-VERIFY the rebuilt lock equals the one held
  pre-departure (GET_RESMASTER exposes neither mode nor handle). ABI + asserts in
  both headers; dispatch both backends; census-reconciled up-front -- the
  vms_kif_dlm_get_granted wrapper is declared OVMX-UNWIRED (scsd issues it
  directly), so kif_caller_census stays green.

vms.ko + userspace build clean; ctest kif_caller_census passes. The scsd collect
send (peer_by_csid + REBUILD, SEND SITE TABLE) + the 3-node h10b harness follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ 3-node harness (a cross-node lock survives its master's departure)

The scsd userspace half of H10b, on top of the already-committed executive
(op-5 REBUILD dispatch + VMS_IOCTL_DLM_GET_GRANTED, 8ea2c98): node A holds a
cross-node lock on RES_C (mastered by C); when C departs gracefully, A
re-registers that SAME lock -- its REAL req_lkid/mode/req_csid, read fresh via
GETLKI, never assumed -- on RES_C's new directory master via a TARGETED
SCS_DLM_OP_REBUILD send. The new master reconstructs it directly into
res->granted and the harness value-verifies the readback three-way against
what A sent.

scsd.c (src/vmsscs/scsd.c):
- peer_by_csid(): the first DLM sender that must target a peer OTHER than the
  one whose join-tick context drove the call (mirrors scsd_peer_by_sys's
  by-identity lookup, by CSID instead).
- scsd_dlm_client_send_rebuild(): the targeted REBUILD send -- a NEW send site,
  registered in the CHOKED SEND SITE TABLE (scs_send_sites census green).
- scsd_dlm_h10b_read_own_mode() / scsd_dlm_h10b_get_granted(): direct-ioctl
  GETLKI / VMS_IOCTL_DLM_GET_GRANTED readbacks, mirroring the existing H9/H10a
  idiom (scsd is a glibc process; these are OVMX-UNWIRED like their kif
  siblings, so kif_caller_census stays green).
- scsd_dlm_h10b_rebuild_on_depart(): the driver, hooked into the SAME
  class-0x04 self-departure receive path H10a's ingress uses -- runs only on
  the node that actually holds the lock (dlm_h10b_hold_established).
- scsd_dlm_send_enq(): new optional OVMX_DLM_ENQ_CSID targeting env. Measured
  necessity: the receive-side ENQ dispatch masters UNCONDITIONALLY whatever it
  gets (no directory check), so in a 3-node cluster an untargeted send (every
  prior 2-node DLM harness never exercised this) would also hand a bogus
  master role to the wrong peer. Additive; absent, every peer is still a legal
  target as before.
- scsd_dlm_cli_msg_input(): the GRANT branch that establishes A's real
  pre-departure hold (dispatches into A's own executive, so GETLKI reads a
  genuine origin record).
- scsd_dlm_srv_msg_input(): the REBUILD receive branch (new master side) --
  dispatch + VMS_IOCTL_DLM_GET_GRANTED readback + SCSD-I-DLMREBUILT marker.

Harness (copy-extend of the H10a rung, #927):
- tests/qemu/init_dlm_h10b.sh, run_dlm_harness_h10b.sh,
  Dockerfile.dlm-harness-h10b -- 3 real-executive QEMU nodes A/B/C on a shared
  mcast fabric; A holds RES_C EX via C (SCSD-I-DLMHOLDOK), C departs
  gracefully, A rebuilds onto the survivor B (SCSD-I-DLMREBUILDSENT), B
  reconstructs + reads back (SCSD-I-DLMREBUILT). Runner asserts three-way value
  equality (sent lkid/mode == read-back lkid/mode, read-back holder_csid ==
  A's CSID) and the new-master target is the survivor, never the departed node.
- .github/workflows/ci.yml: new dlm-harness-h10b job + path filters, mirroring
  the h10a job shape.

PROVEN on a real 3-node QEMU cluster (KVM, docker as build/test tooling only,
Rule 9): DLM HARNESS H10b PASSED, exit 0. SCSD-I-DLMHOLDOK master_csid=1032
mode=EX; SCSD-I-DLMREBUILDSENT csid=1031 lkid=0x00000001 mode=EX sent=1;
SCSD-I-DLMREBUILT found=1 n_granted=1 holder_csid=1030 lkid=0x00000001
mode=EX. scs_send_sites (113 checks) and kif_caller_census both green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared src/kernel-core/vms_lock.c dispatch references VMS_DLM_OP_REBUILD on
BOTH backends, but the executive commit added the define only to the Linux
vms_ioctl.h — amd64-Linux built clean while the NetBSD/amd64 cross-compile
failed with 'use of undeclared identifier VMS_DLM_OP_REBUILD' (vms_lock.c:2298).
The amd64-green-not-twin-proven trap: the NetBSD full-module cross-compile is the
authoritative gate. Symbol-diffed every VMS_DLM_OP_* / VMS_IOCTL_DLM_* / struct
vms_dlm_* the shared TU uses against the mirror — REBUILD was the only gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@baron-3dl
baron-3dl merged commit 46b2eef into main Aug 29, 2026
118 checks passed
baron-3dl added a commit that referenced this pull request Aug 30, 2026
…(the finale) (#930)

* vms-ec75: design note — DLM distributed deadlock detection (rung H11)

Edge-chasing deadlock search over the distributed wait-for graph: a new
SCS_DLM_OP_DLKSRCH probe follows the wait-for edges the master already computes
(blocking_csid/blocking_req_lkid), detects a REAL cross-node cycle, and aborts
one deterministic victim (the initiator) with SS$_DEADLOCK via the existing
GRANT-reply path. Clean-room: structure from standard edge-chasing + public
OpenVMS lock-management docs; the byte-level frame is an OVMX design choice
(Rule 8). Implementation + 2-node cycle harness follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vms-ec75: fix design — GLOBAL-min victim selection, not local-initiator (double-victim hazard)

'Victim = the initiator' is unsafe: in a 2-node cycle both ends can initiate a
search concurrently, each probe returns to its own initiator, and BOTH abort ->
two processes killed when exactly one should be (the data-integrity failure this
rung prevents). Fixed: the probe carries a running MIN (csid,lkid) over every
request it chases; edge-chasing visits the whole cycle before closing, so every
probe computes the SAME global-min victim regardless of who initiated. Cycle-close
tests the initiator; the ABORTED request is the global min. Victim delivery is
idempotent (a second VICTIM for an already-aborted request is a no-op).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vms-ec75: revise chase — CSID-keyed master-and-home-centric, not proc-based (H11 IS buildable)

The Opus impl pass correctly found the proc-based chase framing (find the holder's
process P, does P have a waiting request) unbuildable: a cross-node lock has no
owning process. But that walled framing was the design's error, not a real
prerequisite. The distributed wait-for graph is READABLE today, CSID-keyed and
split across two authorities that existing state already holds:
  - HOME: grant_recv creates a vms_dlm_origin even for a QUEUED reply
    (granted_mode==NL, carrying resnam+master_csid) — so a node's pending
    cross-node waits are enumerable (confirmed: scsd's queued-reply path,
    dlm_pend_seen). New readback ioctl reads the existing origin list.
  - MASTER: res->granted is keyed by req_csid; H10b's VMS_IOCTL_DLM_GET_GRANTED
    already returns holder_csid/holder_req_lkid for a resource — reuse it.
The chase queries home (what does CSID H wait for) then master (who holds it),
hop by hop, reading only real state. No P2 app-acquisition path, no stored blocker
edge — the 2-node cycle is daemon-choreographed + CSID-keyed like every DLM rung.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vms-ec75: DLM rung H11 -- cluster-wide distributed deadlock detection

The LAST DLM rung. Extends the local wait-for-graph detector to a
cluster-wide one: a deadlock CYCLE whose edges cross nodes is detected by
an edge-chasing search (Chandy-Misra-Haas) over REAL executive state, and
EXACTLY ONE deterministic victim's queued $ENQ is aborted with SS$_DEADLOCK
(3594), breaking the cycle.

Executive (shared kernel-core + both backends):
  - SCS_DLM_OP_DLKSRCH = 6 wire op, registered in all N sites: scs_dlm.h
    enum + scs_dlm.c codec/op_name, vms_ioctl.h VMS_DLM_OP_* mirror AND the
    NetBSD mirror vms_lock_nb.h (#928 twin trap), scsd.c static_assert,
    vms_lock.c dispatch. Phase (SEARCH-HOLDER / SEARCH-RESOURCE / VICTIM)
    rides the frame `flags`; ttl rides `status`; the running global-min
    victim rides the valblk slots.
  - New readback ioctl VMS_IOCTL_DLM_ENUM_WAITS (0x38): the HOME authority --
    enumerate this node's pending (NL) cross-node origins (resnam + master +
    handle), one outgoing wait-for edge each. Reuses GET_GRANTED (0x37) as
    the MASTER authority ("who holds R?"). Both wired on Linux + NetBSD; a
    vms_kif_dlm_enum_waits wrapper holds the kernel-floor.
  - The VICTIM leg is dispatched into the executive (vms_lock_dlm_xnode_
    dlksrch): find the queued cross-node waiter (req_csid,req_lkid) on
    res->waiting, remove it, complete with SS$_DEADLOCK. Idempotent.

Daemon (scsd): the initiate (at the master where a cross-node $ENQ queues
behind a REMOTE holder), the two-phase chase orchestration bouncing
home<->master by CSID (peer_by_csid) over the two readback authorities, the
GLOBAL-min victim selection (not local-initiator -- avoids the double-abort
hazard), and the abort + unprompted GRANT(SS$_DEADLOCK) to the victim's
$ENQ. Two new CHOKED senders (scsd_dlm_send_dlksrch probe forward +
scsd_dlm_send_victim_grant victim signal), both in the SEND SITE TABLE.

Harness (3-node A/B/C, C masters both contended resources so both holds and
both waits are cross-node): init_dlm_ec75.sh + run_dlm_harness_ec75.sh +
Dockerfile.dlm-harness-ec75, and the dlm-harness-ec75 CI job.

PROVEN ON A REAL 3-NODE QEMU CLUSTER (KVM): a genuine cross-node cycle
(A waits-for B on RES_E, B waits-for A on RES_D) was DETECTED --
SCSD-I-DLKCYCLE on C (initiator=1030, closed on holder 1030) -- and EXACTLY
ONE victim aborted: node A's WAIT $ENQ returned status=0x00000E0A
(SS$_DEADLOCK), node B's request stayed queued. Every hop reads real state
(enum/get_granted return SS$_NORMAL off live ioctls); INV-6 holds -- a
dropped/ttl-expired probe reports no deadlock, never a fabricated cycle.
Censuses green: send-sites 117/0, kif caller PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl added a commit that referenced this pull request Aug 30, 2026
…der is COMPLETE (H0→H11) (#931)

The cluster-dlm.yaml was last_reviewed 2026-08-28, before H10b/e84/H11 landed — it
still read remaster/LVB/deadlock 'absent' and BLKAST 'deferred', which the merges
(#918 BLKAST H6, #923/#925 LVB H8/H9, #927/#928 remaster H10a/H10b, #929 e84
directory-guard, #930 H11 deadlock — all on main) made false. Update all four
items partial→verified with verified_against pointing at the multi-node /dev/vms
harnesses that prove them, rewrite the summary to the complete ladder + the honest
remaining DEPTH (vms-04f concurrent-initiate proof, vms-b96 directory REDIRECT,
vms-d1f post-1.0 app-acquisition), add REBUILD=5/DLKSRCH=6 to the opcode set, and
render_compat.py (docs/compatibility-surface.md regenerated, drift gate clean).
Closes the vms-7fa epic on the compat surface.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl added a commit that referenced this pull request Aug 30, 2026
* vms-0dd: executive parent-lock linkage + record-lock design (foundation)

Half (a) of RMS record locking: the DLM engine now HONORS the parent lock. The ABI
already carried it (vms_kif_enq parid, vms_enq_args.parid, vms_getlki_args
parent_id) but the executive dropped it (args.parent_id = 0 /* TODO */). Now:
  - vms_lock_entry gains a parent_id field (BOTH src/kernel/vms_internal.h and the
    NetBSD mirror src/kernel-netbsd/vms_internal.h -- the #928 twin trap),
  - vms_enq_core_ex stores it from args.parid at lock creation,
  - GETLKI reports lock->parent_id instead of 0.
Purely additive: every existing  passes parid=0 (a root lock), so the H0->H11
DLM harness + the vms-50e file-lock (both parentless) are unchanged -- the no-parent
path must stay green (the regression net). The cross-node origin GETLKI path keeps
parent_id=0 (an origin record has no local parent). Parent-child AUTO-RELEASE
cascade is deferred (vms-489); RMS releases records explicitly, so it is not needed.

Plus docs/design-rms-record-lock.md: the record-lock protocol (half b) -- RFA-named
per-record , parent = the file-access lock (vms-50e), mode from rab$l_rop
(NLK->none / RLK->PR / default->EX), seam at the rms_record.c dispatch level
(one record lock per stream), conflict -> RMS$_RLK. Implementation of half (b)
follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vms-0dd: fix record-lock design — RLK/NLK are READ MODIFIERS, not lock modes

Conductor caught a plausible-but-wrong mapping (my first cut had RLK→PR). The RMS
status codes are the oracle: RMS$_OK_RLK 'record successfully read, record locked'
proves RAB$M_RLK = READ-THROUGH of a locked record, not a shared read lock.
Corrected: DEFAULT $get → EX (the locking read; second stream → RMS$_RLK); NLK →
NO lock (non-locking read); RLK → read-through (probe the record's real lock state
via NOQUEUE EX; locked-by-another → read + RMS$_OK_RLK, else read + NORMAL) — the
OK_RLK-vs-NORMAL split decided by REAL DLM state (INV-6). The proof must exercise
the RLK read-through + NLK VARIANTS, not just the default→RMS$_RLK conflict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vms-0dd half (b): RMS RECORD-level locking behind the DLM

Completes RMS-behind-DLM (vms-407-B) after vms-50e's FILE-level share
arbitration and this item's half (a) executive parent_id linkage
(ca673c87): a RAB's record-locking intent (rab$l_rop) now takes a real
per-record $ENQ, a CHILD of the FAB's file-access lock, so two streams
contending for one record are arbitrated by the real executive lock
manager -- never a userspace record table (INV-6).

Corrected read-modifier model (RAB$M_NLK/RAB$M_RLK are modifiers, not
lock modes -- the RMS status codes are the oracle), wired centrally at
the rms_record.c dispatch level:
  - default $get/$find: EX $ENQ (parid = file lock). Conflict -> RMS$_RLK.
  - RAB$M_NLK: no $ENQ at all, a dirty read.
  - RAB$M_RLK: throwaway EX/NOQUEUE probe decides RMS$_OK_RLK (read-through
    a genuinely locked record) vs normal (probe granted, immediately DEQ'd).
  - $put: EX-locks the freshly written record.
  - $update/$delete: gated on the stream's own stashed record lkid ->
    RMS$_CUR if it holds none.
  - Release: next $get/$find, RAB$M_NLK, sys$connect/sys$disconnect.

New RAB._rec_lock_lkid field (rab.h) holds one record lock per stream.
The record locator is the byte offset every org get/find handler already
computes (rab->_last_rec_offset) -- no org handler in this tree populates
rab$w_rfa, so this is OVMX's own construction over the public "one
resource per record" contract, not a disclosed RFA encoding (Rule 8).

vms_kif_getlki_parent (vms_kif.c/.h) is a new sibling of vms_kif_getlki
that also surfaces the GETLKI parent_id field ca673c87 wired into the
kernel but the original wrapper never exposed -- same ioctl, no new op,
every existing vms_kif_getlki call site untouched.

Upgraded rms_record.c's and rms_core.c's OVMX userspace-service-register
declarations (sys$get/put/update/delete/find, sys$connect/disconnect) to
reflect the new executive reach; both standing gates
(test_userspace_service_register.sh, test_kif_caller_census.sh) pass.

Proof: tests/qemu/test_syssvc_rms_reclock.c, run against a real vms.ko +
/dev/vms + the writable real-VAX ODS-2 fixture on DKA0: (a hand-built
minimal QEMU harness mirroring tests/qemu/run_tests.sh, given constrained
shared-host disk) -- 32/32 assertions PASS, rc=0. The sibling
test_syssvc_rms_filelock.c (vms-50e, parentless path) stayed 16/16 PASS,
rc=0 in the same boot -- the regression net holds. Full host debug build
(all targets) is clean; ctest -R 'rms|dcl' is 22/22 passed (1 unrelated
pre-existing honest skip, dcl_acceptance_e2e, no /dev/vms on host).

Deviations from the original design draft (superseded mid-session by the
corrected read-modifier model, and independently during implementation):
rab$w_rfa is not populated (nothing in the tree reads it; the byte-offset
locator is used directly instead); sys$put's record-lock is best-effort
(a fresh record cannot realistically conflict); sys$close has no RAB
back-pointer so its record-lock release seam is sys$connect/disconnect
only, matching the existing sys$close/RAB architecture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vms-0dd fix: $PUT holds no record lock (un-break test_syssvc_rms_acp regression)

CI caught a real regression my local harness missed: test_syssvc_rms_acp (write
8 records, close, reopen, read back) + test_syssvc_mmk_drive went red. Root cause:
rms_reclock_after_put stashed an EX record lock, but (1) rms_seq_put never sets
_last_rec_offset so every sequential $PUT collided on resource RMSR<fid>.0.0, and
(2) a record lock outlives the file lock (auto-release cascade deferred, vms-489)
while $CLOSE cannot reach the RAB to release it (vms-3ce) AND the test re-inits the
RAB (rab = cc$rms_rab) after reopen, zeroing _rec_lock_lkid without releasing. So
the $PUT lock on R.0.0 LEAKED, and the read-back's $GET of record 1 (offset 0) hit
it -> RMS$_RLK -> byte-exact read failed.

Fix: $PUT holds NO persistent record lock -- rms_reclock_after_put now only
RELEASES the stream's current-record lock and takes none. A $PUT lock would name a
stale/colliding resource anyway (no per-record put locator), and holding it is
what leaks. A record lock a later $UPDATE needs is taken by the preceding $GET (the
VMS pattern the done-condition exercises). $PUT-then-$UPDATE-same-stream without an
intervening $GET waits on per-record put locators + vms-489 (tracked in vms-3ce).
$GET/$UPDATE/$DELETE record locking + the reclock proof are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vms-0dd fix: implement the parent-child auto-release cascade (un-break dcl_acp get-lock leak)

The $PUT-lock fix cleared rms_acp + mmk_drive, but CI then caught a SEPARATE
get-lock leak: test_syssvc_dcl_acp (COPY a file, close, reopen, re-read byte-exact)
red. A $GET record lock is released on the next $GET (one per stream), but the LAST
one held survives $CLOSE ($CLOSE can't reach the RAB, vms-3ce), and the test's RAB
re-init on reopen (rab = cc$rms_rab) drops the lkid WITHOUT releasing -> the record
lock leaks and blocks the file's own re-read (RMS$_RLK). A leaked lock blocking
legitimate access is exactly the failure this rung exists to prevent.

Fix = the vms-489 cascade, now required (not deferrable): release_child_locks()
in vms_deq_core releases a lock's child sublocks (locks whose parent_id == the
deq'd lkid) on the same process. RMS record locks carry their file-access lock as
parent, so $CLOSE (which $DEQs the file lock) now releases every record lock still
held under it -- the record lock no longer outlives its file. Mirrors
vms_proc_release_locks' safe iteration over proc->locks; a leaf lock (no children)
makes it a cheap no-op, so the parentless H0->H11 + reclock + file-lock paths
(parid=0, no lock has parent_id==their lkid) are byte-unchanged. Shared kernel-core
-> both substrates; no header/ABI change. INV-6: real teardown, never a fake clear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl added a commit that referenced this pull request Aug 30, 2026
… executive (fix CREPRC in booted runtime) (#961)

SPAWN failed with %DCL-F-CREPRC in the booted runtime: interactive DCL runs
non-root (LOGINOUT drops to the user's UIC), so its $CREPRC child touched the
executive before image activation and registered FRESH -> unprivileged, empty
username -> then tried vms_kif_setident("SYSTEM", full-privs), which the
executive correctly refused with SS$_NOPRIV (a non-root caller cannot
self-declare a privileged name). The kernel spawn suites passed only because
their creator was root and unnamed, so they never hit this branch
(green-in-isolation, broken-in-runtime).

Fix: the subprocess obtains identity by CONTINUATION from its unforgeable
real_parent, not by re-stamping. New VMS_IOCTL_REGISTER_SUBPROCESS path:
the executive reads the SPAWNing DCL's row and copies its CURRENT (possibly
reduced) UIC/username/privilege masks onto the child, giving the subprocess
its OWN PID (unlike image-activation _CONTINUE which shares the parent PID).
Identity stays parent-derived, never caller-declared (INV-6 / executive
boundary). The SS$_NOPRIV setident guard is UNCHANGED — a non-root caller
still cannot self-award a privileged name; we fixed how the child gets its
identity, not the guard.

New symbol wired through all required places: VMS_IOCTL_REGISTER_SUBPROCESS
(vms_ioctl.h), vms_kif_register_subprocess (vms_kif.c/.h + libvmssys_shr.vec),
handler in vms_module.c, and BOTH NetBSD twins (vms_netbsd.c +
vms_proctab_nb.h, #928 twin trap).

Coverage: tests/qemu/test_syssvc_creprc_inherit.c reproduces the runtime
condition the old suites missed — a NAMED, NON-ROOT creator whose $CREPRC
child must inherit the creator's identity. Fails before this change, passes
after. VMS User Acceptance battery goes 62/4 -> 66/0 under KVM (the 4 SPAWN
assertions now pass).

Executive-boundary/kernel-module-interface change -> Design Change Cascade:
API compat = additive (new ioctl, no existing ABI changed); test coverage =
new regression test above; doc impact = minimal.


Claude-Session: https://claude.ai/code/session_01SwfchU4UcPzfdkLL2JSV4T

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl added a commit that referenced this pull request Sep 2, 2026
…ader (vms-25e)

The Layer 1 change (ee4d1f3) declared vms_lock_acp_vol_standing only in
src/kernel/vms_internal.h (Linux). kernel-core's vmsfs_acp.c resolves
"vms_internal.h" per build, so the NetBSD/vax cross-compile hit
  vmsfs_acp.c:724: implicit declaration of 'vms_lock_acp_vol_standing'
(-Werror). Mirror the declaration into src/kernel-netbsd/vms_internal.h next to
vms_lock_acp_vol_ex, exactly as #928 requires for kernel-core lock ops. Linux
build + the QEMU standing-lock test were already green; this restores the VAX
arch leg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhM3QcmFEY3p8YNnHGaYwP
baron-3dl added a commit that referenced this pull request Sep 2, 2026
…em locks (vms-1f4) [Layer 2]

Layer 2 of faithful cluster DLM registration (vms-3eb): the clean seam between
the F11 layer that HOLDS the standing volume lock (Layer 1, vms-25e) and the
cluster layer (scsd) that REGISTERS it to the coordinator. A new read-only ioctl
returns this node's standing cluster-registrable locks -- today one entry per
mounted volume holding its F11B$v<label> lock: {resname, local lkid, mode}. The
lkid is exactly the op-0x01 requester handle scsd puts on the wire (Layer 3).

INV-6: a READ of REAL lock state. count is the number of standing locks the
executive genuinely holds; a volume whose best-effort acquire failed
(vol_lkid == 0) contributes nothing; never a fabricated lock.

Wired across all nine places a new /dev/vms ioctl touches (the "new-ioctl -> N
places" trap), each verified consistent:
- struct + ioctl def (NR 0x3c, next free after the 0x30-0x3b DLM/cluster group --
  0x39 was already CLUSTER_MEMBER_SET, caught before it collided) + size/encoding
  asserts, in BOTH src/kernel/vms_ioctl.h and src/kernel-netbsd/vms_lock_nb.h,
  byte-identical (656 bytes);
- handler vms_ioctl_dlm_enum_standing() in kernel-core/vmsfs_acp.c (walks the
  executive-global mounted-volume table under vms_acp_vol_lock, copyout outside
  the lock); handler decl in BOTH vms_internal.h (#928);
- dispatch in kernel/vms_module.c and both switches of kernel-netbsd/vms_netbsd.c;
- kif wrapper vms_kif_dlm_enum_standing() decl + impl (libvmssys), fail-honest.

Test (test_syssvc_acp_dlm): after $MOUNT, DLM_ENUM_STANDING surfaces the
F11B$v<label> lock with a real (nonzero) handle -- the (resname, lkid) pair scsd
will register. LCK_K_NLMODE + the struct are reachable in both the Linux and
NetBSD builds of the shared TU (verified, not assumed). Layer 3 (vms-74f) wires
scsd to register these to the coordinator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhM3QcmFEY3p8YNnHGaYwP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant