Skip to content

Bind the product to the executive: vms_kif_register() had zero callers, so every /dev/vms ioctl failed (vms-9fc) - #16

Merged
baron-3dl merged 6 commits into
mainfrom
work/vms-9fc
Jul 30, 2026
Merged

baron-3dl merged 6 commits into
mainfrom
work/vms-9fc

Conversation

@baron-3dl

Copy link
Copy Markdown
Contributor

The shared substrate defect under every Phase 1 and Phase 3 item. vms_kif.h's own header documents the protocol — "1. open /dev/vms 2. Call vms_kif_register()" — and step 2 had zero callers product-wide. vms_module.c routes every ioctl except REGISTER through a lookup that returns -ESRCH for an unregistered process, so every /dev/vms call from the product failed.

The precedent was broken too

sys_lock.c is described throughout this epic as "the ONE facility already wired to /dev/vms", and every implementer was told to copy it. Its bind called vms_kif_open() and only vms_kif_open() — the same omission. So sys$ENQ/$DEQ was failing in the shipped product and had never been proven end-to-end. Copying the precedent propagated the defect.

Fixed once, at the KIF layer: kif_bind() completes open→register before every ioctl, and all 28 wrappers route through it. bind_to_executive() is deleted rather than repaired — per-facility binding is exactly what let four callers each forget the step.

One PCB per PROCESS, not per Linux thread

The first cut keyed binding on gettid(), which minted one VMS PCB per thread. Review proved it in QEMU: a pthread saw prcnam='' after the main thread's $SETPRN returned success, and event flag 41 clear after the main thread set it. That is the Rule 11 facade inverted — per-thread state pretending to be per-process. On OpenVMS a process has one PCB shared by all its threads; that is the entire meaning of a process-wide event flag cluster.

Now keyed on tgid on both sides, with vms_dev_release() gaining a whole-process liveness test so a thread exiting out of a live image cannot delete its process's PCB. Proven: a pthread using only public entry points sees the same executive entry, name, flag state, and can $DEQ the main thread's lock — 42/0. Minimal mutation tgid → pid: exactly 5 assertions red, all in the thread suite, every other suite green.

Post-exec register: ADOPT

Oracle-pinned, not chosen. VAX 7.3, SHOW PROCESS/ACCOUNTING across two further image activations: same Process ID 2020021D, same name, Images activated 19 → 21, no error. vms_ioctl_register() now adopts a task it already knows and returns SS$_NORMAL; the old 0x1C is deleted. Adoption deliberately does not re-apply init_privs — a process that can reset its own privileges by re-registering is declaring its own privileges.

A ninth wrong constant

SS$_ILLIOFUNC was 580 in both ssdef.h and vms_errno.h. The oracle says 244, and 580 is %SYSTEM-F-VASFULL — so every sys$qio rejecting an unimplemented function code reported "virtual address space is full." The claim that "consumers name the symbol, so none breaks" was false: dcl_lexical.c's F$MESSAGE table is keyed by number. Corrected in both copies plus the message table and docs, and bound with a _Static_assert so moving it again breaks the build.

A negative control that silently lied

test_runtime_target_negctl.sh went 13/20 with all seven check-3c controls reporting "the evasion was CERTIFIED, not caught." The gate was fine — the control was broken: its fixture injected into a function this branch deletes, so the injection became a silent no-op and the gate ran on an unmutated tree. Fixed structurally, not per-instance: expect_red now takes the file its injection should have changed and reports BROKEN FIXTURE (not a broken gate) if that file is byte-identical, before it will interpret any verdict. A meta-control uses the real dead anchor. 21/0.

Note on the evidence

A reviewer refuted the implementer's own proof: test_syssvc_lock.c bootstraps its own registration, so it reports 4/0 on main byte-identically and does not discriminate for this item at all. It then wrote an independent probe using only public sys$ calls and confirmed the underlying claim holds on the branch and fails on main. Conclusion survived; the evidence for it was replaced.

ci.yml's negative-control floor was recomputed from the merged tree by building the image — neither side of the conflict taken, since PR #15 had replaced the tally pin with per-suite verdicts.

🤖 Generated with Claude Code

baron-3dl and others added 6 commits July 30, 2026 14:03
…s (vms-9fc)

vms_kif.h documented "open /dev/vms, then vms_kif_register()" from the day
it was written. vms_kif_register() was defined and had ZERO CALLERS product-
wide, and vms_module.c routes every ioctl except REGISTER through
vms_proc_find_or_err(), which answers -ESRCH for an unregistered task. So
every /dev/vms call OVMX made was rejected -- including sys$ENQ/$DEQ in
sys_lock.c, the facility the executive-retrofit design named as "already
wired" and told every implementer to copy. sys_lock.c's bind_to_executive()
called vms_kif_open() and only vms_kif_open(), so the omission was
propagated by design review rather than caught by it.

The sequence now lives once, in kif_bind() at the kernel-interface layer,
so every facility built on it inherits a registered process. It is keyed by
task id, not a boolean: fork() copies TLS wholesale, so a child inherits
both the parent's descriptor and the parent's "bound" mark while the
executive knows it is not registered. The same comparison covers a new
thread and a freshly activated image after execve().

REGISTER now ADOPTS a task the executive already knows, returning
SS$_NORMAL instead of 0x1C, and does not re-apply the requested privileges.
ORACLE PIN (reference lab VAX1, OpenVMS VAX V7.3, 2026-07-30): activating an
image inside a process does not recreate the process and is not an error --
SHOW PROCESS/ACCOUNTING across two further activations reports the same
Process ID 2020021D and name "SYSTEM" with Images activated 19 -> 21.

Failed ioctls no longer all report SS$_BADPARAM ("your parameters were
bad") to callers whose parameters were fine. The errno set is closed
because both sides of /dev/vms are ours, and each mapped status is oracle-
pinned by $SSDEF extraction plus F$MESSAGE round-trip: -EFAULT ->
SS$_ACCVIO 12, -ENOMEM -> SS$_INSFMEM 292, -ENOTTY -> SS$_ILLIOFUNC 244.
-ESRCH gets no status of its own (Rule 10: VMS is never without a PCB, so
kif_bind makes it unreachable); if seen anyway it is SS$_BUGCHECK 676,
"internal consistency failure".

Same oracle run corrects SS$_ILLIOFUNC from 580 to 244 in ssdef.h and
vms_errno.h: F$MESSAGE(580) is %SYSTEM-F-VASFULL, so every sys$qio that
rejected an unimplemented function code was reporting address-space
exhaustion.

tests/qemu/test_kmod_bind.c proves it against a real /dev/vms in QEMU. Its
positive path never calls vms_kif_open() or vms_kif_register() before using
a facility -- supplying the step the product forgets is exactly how this
defect stayed invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ontrols (vms-9fc)

Two things the standing gates caught in the first commit, both correctly.

1. tests/integration/test_runtime_target.sh flagged `if (vms_kif_open() < 0)
   return;` in kif_bind(): branching on whether the executive could be opened
   is the deleted fallback in a new place. The condition is unreachable by
   construction -- PID 1 refuses to bring the system up without /dev/vms --
   so under Rule 10 it must not be handled at all. The open is now
   unconditional and its result discarded. If a descriptor somehow is not
   available, REGISTER fails, vms_bound_tid stays 0, and the caller gets
   SS$_BUGCHECK; nothing is ever told it is bound when it is not.

2. tests/integration/test_runtime_target_negctl.sh went 13/20: all seven of
   its check-3c controls reported "the evasion was CERTIFIED, not caught".
   The gate was fine. The CONTROL was broken: its fixture injects an evasion
   into sys_lock.c's bind_to_executive(), a function this branch deletes, so
   the injection silently became a no-op and the gate was being run against
   an unmutated tree. Re-anchored to src/libvmssys/vms_kif.c's kif_bind(),
   which is where binding lives now. 20/20 again, with the same seven
   evasions genuinely caught.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 1 wired the product to the executive but keyed the binding on the
THREAD: kif_bind() marked itself with vms_sys_gettid() and vms_module.c
registered and looked up on current->pid. Every thread that touched any
vms_kif_* entry point therefore minted its own executive process. Measured
in QEMU by the veracity adversary: a pthread of one image saw linux_pid=119
while getpid()=117, saw prcnam='' after the main thread's $SETPRN returned
SS$_NORMAL, saw event flag 41 CLEAR after the main thread SET it, and could
not $DEQ its own process's lock.

That is not VMS. A VMS process has ONE PCB and its kernel threads SHARE it
-- that shared residency is the entire meaning of a process-wide event flag
cluster, a process name and a process's lock ids. It is Rule 11's facade
shape inverted: per-thread state pretending to be per-process. It was also
newly reachable, because before round 1 nothing registered at all.

Fixed at the identifier, on both sides of /dev/vms:
 - src/kernel/vms_module.c keys the process table on current->tgid and pins
   task_tgid(current), the Linux process-wide identity (== getpid(2)).
 - src/libvmssys/vms_kif.c's TLS mark records vms_sys_getpid(), so userspace
   and the executive agree on what the registration is a property of. The
   fork/exec/new-thread cases the mark exists to catch all still work: a
   forked child's inherited mark mismatches its new pid, and fresh or wiped
   TLS reads 0.
 - vms_dev_release() no longer deletes a live process's PCB when one of its
   threads exits: it now requires the last member of the thread group.
   Anything that misses is reaped lazily as before, and the reaper's
   liveness test is now whole-process (the pinned tgid resolves to the group
   leader, which the kernel does not release while any thread runs).

PROVED, not asserted. tests/qemu/test_kmod_bind.c gains suite 7: the main
thread names the process, sets a local event flag and takes a lock, then a
pthread -- using only public entry points, no explicit open or register --
must report the SAME executive entry, the SAME name, the SAME flag state and
must be able to release the main thread's lock. Against a real /dev/vms with
vms.ko insmod'd: 42 passed, 0 failed; whole harness 14 suites passed, 0
failed.

MINIMAL MUTATION for this property and no other -- vms_module.c
current->tgid -> current->pid, rebuilt and rerun in QEMU: exactly 5
assertions red, ALL in suite 7 (37 passed / 5 failed), every other suite
still green including vms-8019's test_kmod_procnam (30 passed, 0 failed).
No single-threaded test can see this defect, which is why suites 1-6 and
every other kernel suite pass either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-9fc)

Round 1 corrected SS$_ILLIOFUNC 580 -> 244 in ssdef.h and vms_errno.h and
claimed "consumers name the symbol, so none breaks". That was FALSE. DCL's
F$MESSAGE table in dcl_lexical.c is keyed by NUMBER and hard-coded
{ 580, 'E', "ILLIOFUNC" }, so on the corrected tree:
    X = F$MESSAGE(244) -> "%SYSTEM-?-UNKNOWN, message code %X000000F4"
    X = F$MESSAGE(580) -> "%SYSTEM-E-ILLIOFUNC, illegal I/O function"
OVMX could not name the status its own sys$qio returns, and still rendered
"illegal I/O function" for what the oracle calls VASFULL. Severity was wrong
too ('E' where the oracle says 'F').

ORACLE, RE-RUN THIS SESSION on the reference lab (OpenVMS VAX V7.3, node
VAX1 -- not recalled from the previous round's transcript), two ways:
    LIBRARY/EXTRACT=$SSDEF/OUTPUT=... SYS$LIBRARY:STARLET.MLB + SEARCH
        $EQU  SS$_ILLIOFUNC   244
        $EQU  SS$_VASFULL     580
    F$MESSAGE
        244 -> %SYSTEM-F-ILLIOFUNC, illegal I/O function code
        580 -> %SYSTEM-F-VASFULL, virtual address space is full

So: a 244/'F'/ILLIOFUNC row is added, the 580 row becomes VASFULL/'F',
status.c's ILLIOFUNC text gains the oracle's "code", and
docs/api-system-services.md's 580 row is corrected.

BOUND SO IT CANNOT DESYNCHRONISE AGAIN. A _Static_assert in dcl_lexical.c
ties the table's 244 row to SS$_ILLIOFUNC itself, so moving the constant
again breaks the build instead of silently leaving F$MESSAGE unable to name
a status OVMX hands out.

TESTS, run not asserted (ctest 40/40 pass, 0 failed):
 - new tests/dcl/test_lexical_message.sh asserts the round trip through the
   built DCL.
 - three mutations, each rebuilt and rerun: dropping the 244 row reproduces
   the adversary's exact "%SYSTEM-?-UNKNOWN, message code %X000000F4";
   restoring 580/'E'/ILLIOFUNC turns the two VASFULL assertions red and
   nothing else; setting SS$_ILLIOFUNC back to 580 fails the build on the
   static assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 1's re-anchor of check 3c was correct but treated the symptom. The
COUPLING is structural: every control here is a sed/awk injection anchored
to a source line, and an anchor that stops matching -- a renamed, reshaped
or deleted function -- makes the injection a SILENT NO-OP. The sandbox stays
unmutated, the gate correctly stays GREEN, and every downstream control then
reports "the evasion was CERTIFIED, not caught", blaming the gate for a
broken fixture. That is exactly what happened when this branch deleted
sys_lock.c's bind_to_executive(): 13/20, all seven 3c controls red against a
perfectly healthy gate. A future rename reproduces it verbatim.

So expect_red now takes the file its mutation was supposed to change and
refuses to interpret the gate's verdict until that file demonstrably differs
from its pristine copy, naming the FIXTURE rather than the gate when it does
not. A control that silently tests nothing is worse than none: it reports
that evasions are caught.

The detector gets its own control, using the real anchor that broke
(sys_lock.c's deleted bind_to_executive).

RUN, not asserted: 21 passed / 0 failed (was 20 + the new meta-control). And
verified the detector actually fires -- a copy of this script with 3c's awk
anchor pointed at a function that does not exist reports
"BROKEN FIXTURE (not a broken gate)" for all seven 3c cases instead of
certifying them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 12 -> 13 bump landed in this branch's conflict resolution against
vms-1d9 (PR #15), which had REPLACED the exact-tally pin with per-suite
verdicts plus this floor. Note in the file how the figure was obtained, so
the next branch does not resolve a conflict by merging two numbers.

The figure was verified by building the NEGATIVE_CONTROL=1 image on the
merged tree and booting it under aarch64 QEMU TCG:
  === FINAL RESULTS: 3 suites passed, 12 suites failed ===
  test_syssvc_lock rc=77, test_kmod_vmsfs* rc=0, all other test_kmod_* rc=1
and by replaying ci.yml's own verdict loop over that transcript:
N_EXPECTED=13, N_SYSSVC=1, BAD empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@baron-3dl
baron-3dl merged commit 7c9a9e0 into main Jul 30, 2026
34 checks passed
baron-3dl pushed a commit that referenced this pull request Aug 11, 2026
…ration onto its own line

Root cause of the Build & Test CI failure (test #16
tests/integration/test_userspace_service_register.sh, 1/130): the sys$delprc
declaration comment embedded "OVMX-LOCAL: the actual termination is..." mid-
paragraph inside the OVMX-PARTIAL bullet, instead of as its own tagged line.
The gate's parser requires OVMX-LOCAL: sys$NAME -- ... verbatim (regex
OVMX-LOCAL:\s*sys\$[A-Za-z0-9_]+\s*--), so it never recognized a LOCAL half
for sys$delprc at all -- an OVMX-PARTIAL declaration with no paired
OVMX-LOCAL is exactly the incomplete-declaration RED this gate exists to
catch (docs in the gate's own header: "OVMX-LOCAL -- the other half;
required").

Fix: split the paragraph into two properly-tagged bullets, matching the
existing sys$getjpi/sys$creprc convention in the same file. No functional
change -- sys$delprc's resolution/privilege/termination logic is untouched.

Verified: this is NOT a behavioral regression. tests/qemu/test_syssvc_delprc.c
(21/21, real vms.ko) and tests/dcl/test_stop_facade_gate.sh both still pass
unchanged -- neither exercises this declaration-register gate. Locally:
  bash tests/integration/test_userspace_service_register.sh
now exits 0 / PASS, and lists sys$delprc as "partial ... vms-1a8" in its
service table (previously it drove the gate to FAIL: BROKEN/incomplete
declaration on this exact service).
(userspace_service_register_negctl is de-gated per CI, vms-b44 -- not run
by the Build & Test job; verified separately, unaffected either way since
it mutates the gate script itself, not this file.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl added a commit that referenced this pull request Aug 11, 2026
…318)

* vms-1a8: STOP actually stops a process target (INV-DCL facade kill)

cmd_stop() ignored its target and unconditionally self-exited the CURRENT
DCL session, claiming SS$_NORMAL for a process it never looked at. STOP
<process-name> and STOP/IDENTIFICATION=pid now resolve the target through
the real executive process table and terminate it via sys$delprc, which
itself stops discarding prcnam and treating pidadr as a raw Linux pid
(the OVMX-USERSPACE facade documented at the top of sys_process.c).

- sys$delprc (src/libvms/syssvc/sys_process.c): resolves the target via
  vms_kif_getjpi_prcnam/_pid (same pattern as sys$getjpi), enforces the
  DCL Dictionary's GROUP/WORLD privilege rule for another process (GROUP
  or WORLD authorizes a same-group delete, WORLD alone a cross-group one
  -- the cross-group case is already gated by vms_proc_may_read()'s WORLD
  check at resolution), then SIGTERMs the resolved Linux pid. Authentic
  SS$_NONEXPR/SS$_NOPRIV, never fake success (INV-6).
- cmd_stop() (dcl_cmd_process.c): dispatches process-name / /IDENTIFICATION
  target forms to sys$delprc; bare STOP (no target) is unchanged. STOP now
  carries a qualifier table (q_stop) so /QUEUE, /CPU, /NETWORK -- the
  SEPARATE, unimplemented STOP/QUEUE etc. Dictionary entries -- draw the
  authentic %DCL-W-IVQUAL instead of silent acceptance.

Clean-room citation: OpenVMS DCL Dictionary "STOP" entry
(https://www.mrynet.com/FTP/operatingsystems/VMS/docs/ssb71/9996/9996p062.htm).

Ground-source:
- tests/dcl/test_stop_facade_gate.sh (host, no /dev/vms needed): proves the
  negative half -- never $STATUS=1 for a target form, honest IVIDENT/IVQUAL,
  bare STOP unchanged. Fails on the pre-fix facade (verified), passes post-fix.
- tests/qemu/test_syssvc_delprc.c (real vms.ko under QEMU): P1/P2 create a
  named target from one process and terminate it from another by name and by
  PID, confirming it is actually gone (waitpid reaps it, $GETJPI -> SS$_NONEXPR
  afterward); P3 proves the GROUP-privilege refusal leaves the target ALIVE;
  P4 proves nonexistent targets return SS$_NONEXPR. All 21 assertions pass
  against a real executive; full kernel-executive suite (every test_kmod_*/
  test_syssvc_* program) still green, no regressions.
- docs/dcl-verb-fidelity-scoreboard.md: STOP moved FACADE -> REAL (49 REAL ·
  4 PARTIAL · 0 top-level FACADE · 1 STUB).

DCL suite: 98/100 (2 pre-existing, unrelated INSTALL.EXE/PRODUCT.EXE failures
present identically on main before this change -- verified via git stash).

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

* vms-1a8: fix userspace_service_register gate — split OVMX-LOCAL declaration onto its own line

Root cause of the Build & Test CI failure (test #16
tests/integration/test_userspace_service_register.sh, 1/130): the sys$delprc
declaration comment embedded "OVMX-LOCAL: the actual termination is..." mid-
paragraph inside the OVMX-PARTIAL bullet, instead of as its own tagged line.
The gate's parser requires OVMX-LOCAL: sys$NAME -- ... verbatim (regex
OVMX-LOCAL:\s*sys\$[A-Za-z0-9_]+\s*--), so it never recognized a LOCAL half
for sys$delprc at all -- an OVMX-PARTIAL declaration with no paired
OVMX-LOCAL is exactly the incomplete-declaration RED this gate exists to
catch (docs in the gate's own header: "OVMX-LOCAL -- the other half;
required").

Fix: split the paragraph into two properly-tagged bullets, matching the
existing sys$getjpi/sys$creprc convention in the same file. No functional
change -- sys$delprc's resolution/privilege/termination logic is untouched.

Verified: this is NOT a behavioral regression. tests/qemu/test_syssvc_delprc.c
(21/21, real vms.ko) and tests/dcl/test_stop_facade_gate.sh both still pass
unchanged -- neither exercises this declaration-register gate. Locally:
  bash tests/integration/test_userspace_service_register.sh
now exits 0 / PASS, and lists sys$delprc as "partial ... vms-1a8" in its
service table (previously it drove the gate to FAIL: BROKEN/incomplete
declaration on this exact service).
(userspace_service_register_negctl is de-gated per CI, vms-b44 -- not run
by the Build & Test job; verified separately, unaffected either way since
it mutates the gate script itself, not this file.)

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

---------

Co-authored-by: alice <alice@workspace.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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