Bind the product to the executive: vms_kif_register() had zero callers, so every /dev/vms ioctl failed (vms-9fc) - #16
Merged
Merged
Conversation
…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
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>
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.
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. Callvms_kif_register()" — and step 2 had zero callers product-wide.vms_module.croutes every ioctl except REGISTER through a lookup that returns-ESRCHfor an unregistered process, so every/dev/vmscall from the product failed.The precedent was broken too
sys_lock.cis described throughout this epic as "the ONE facility already wired to/dev/vms", and every implementer was told to copy it. Its bind calledvms_kif_open()and onlyvms_kif_open()— the same omission. Sosys$ENQ/$DEQwas 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 sawprcnam=''after the main thread's$SETPRNreturned 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
tgidon both sides, withvms_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$DEQthe main thread's lock — 42/0. Minimal mutationtgid → 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/ACCOUNTINGacross two further image activations: same Process ID2020021D, same name, Images activated 19 → 21, no error.vms_ioctl_register()now adopts a task it already knows and returnsSS$_NORMAL; the old0x1Cis deleted. Adoption deliberately does not re-applyinit_privs— a process that can reset its own privileges by re-registering is declaring its own privileges.A ninth wrong constant
SS$_ILLIOFUNCwas 580 in bothssdef.handvms_errno.h. The oracle says 244, and 580 is%SYSTEM-F-VASFULL— so everysys$qiorejecting 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'sF$MESSAGEtable is keyed by number. Corrected in both copies plus the message table and docs, and bound with a_Static_assertso moving it again breaks the build.A negative control that silently lied
test_runtime_target_negctl.shwent 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_rednow takes the file its injection should have changed and reportsBROKEN 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.cbootstraps its own registration, so it reports 4/0 onmainbyte-identically and does not discriminate for this item at all. It then wrote an independent probe using only publicsys$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