Skip to content

The QEMU harness can finally see userspace: prove a public sys$ entry point against a real /dev/vms (vms-1d9) - #15

Merged
baron-3dl merged 7 commits into
mainfrom
vms-1d9-round5
Jul 30, 2026
Merged

baron-3dl merged 7 commits into
mainfrom
vms-1d9-round5

Conversation

@baron-3dl

Copy link
Copy Markdown
Contributor

Removes the Phase-0.5 barrier. The kernel-executive CI job proved the kernel side and was structurally blind to userspace — which is the half every Phase 1 and Phase 3 item delivers.

The defect

tests/qemu/Dockerfile copied src/kernel/ and the test sources and nothing else. The initramfs contained zero bytes of the userspace system-service layer. Proven empirically: an adversary reverted src/libvms/syssvc/sys_event.c wholesale to its pre-change stub, rebuilt, reran — byte-identical output, exit 0. Deleting 100% of an item's production code changed nothing the gate could see.

What landed

test_syssvc_lock.c exercises sys$enq / sys$enqw / sys$deqpublic entry points through the real libvms — against a real /dev/vms in QEMU, with a genuine two-process cross-release assertion.

The decisive proof is a facade injection. A reviewer replaced do_enq/sys$deq with a per-process userspace lock table — the exact Rule 11 facade this epic exists to kill — and the first five assertions passed. It failed only at "child: sys$enqw EX granted after parent's sys$deq". A single-process-shaped test would have been fooled; the cross-process release assertion is what catches it. Kernel-side corroboration: dmesg shows registered process pid=112 and pid=113 — two genuinely distinct executive processes.

Three structural fixes, each with its own minimal mutation

  1. The harness is generic now. It wired exactly one binary by literal name, twice — so the next item to add test_syssvc_event.c would have built locally, ctest'd fine, and then been silently not built, not staged and not run, while CI stayed green. Now pattern-dispatched throughout. Proven both ways: a second suite added with zero Dockerfile edits was built, staged and ran; narrowing the cp back to a literal name turned the job RED with "NEVER RAN (no verdict line — not built, or not staged into the initramfs)".
  2. The positive job has a gate (closes vms-d2d). It derives the suite set from the tree and asserts each suite's own verdict line, plus a source-count floor. Proven by deleting test_kmod_ast.c: the harness still printed ALL KERNEL MODULE TESTS PASSED and exited 0 — and the job went RED with "only 11 suite sources (expected at least 12)".
  3. The negative control judges a system service, not a printf. It previously grepped for cannot open /dev/vms, which matched the test's own bootstrap message — an assertion satisfiable by something other than the behaviour under test. It now calls a public sys$ entry point with the device absent and judges what comes back. Proven by injecting a fabricated success: all four assertions failed and the job went RED, while FINAL RESULTS stayed byte-identical — i.e. the hand-maintained tally pin this replaces would have stayed green.

Also fixed

  • The suite could hang the whole VM. The child's post-release sys$enqw blocks in the kernel, and enq_wait_sync re-arms on every signal wake without returning to user mode, so a child-side alarm() is swallowed — measured, not assumed. An unreleased lock sat until the 120 s QEMU timeout, every later suite never ran, and CI saw an unattributable timeout indistinguishable from a boot panic. The bound now lives in the parent, which isn't blocked.
  • tools/replay_ci_kernel_executive.py extracts the ci.yml assertion blocks verbatim and runs them against captured output, so a local proof cannot drift from what CI runs.

Deliberate removals

  • tests/qemu/CMakeLists.txt's add_test() — it reported Skipped in 100% of environments where ctest runs and never ran in the one where it can pass. Rule 10: a permanently-skipped test is a failing test. No coverage lost: add_executable keeps it in all (it still breaks the host build if it stops compiling) and the QEMU job runs and gates it. Host ctest: 40/40, zero skips.
  • lksdef.h dropped — the LKSB has no VMS-published byte layout, so a public header would be an OVMX design choice needing sign-off (Rule 8). The test declares its own LKSB storage, which is what OpenVMS callers do anyway.

Known, disclosed

Production code still has zero callers of vms_kif_register() (vms-9fc, in flight), so this proves the lock plumbing works through the public entry points, not that a normally-activated image can use them yet. Stated loudly in the file header and in ci.yml.

🤖 Generated with Claude Code

baron-3dl and others added 7 commits July 29, 2026 14:50
…vms-1d9)

Phase 0.5 hard barrier: the merged Kernel Executive CI job (vms-e4d) only
COPYs src/kernel/ + tests/qemu/ into its initramfs, so every test drives
/dev/vms with raw ioctls. An adversary proved that reverting a userspace
syssvc file (src/libvms/syssvc/sys_event.c) to its pre-change stub left
the harness byte-identical green -- every Phase 1/3 item is userspace
wiring the harness cannot see at all.

Adds test_syssvc_lock, statically linked against the REAL src/libvms
(musl, same OVMX_STATIC mode distro/Dockerfile.bootable already uses) and
built into the initramfs alongside the raw-ioctl test_kmod_* programs. It
calls the public sys$enq/sys$enqw/sys$deq entry points across a real
fork()'d second process and, empirically (podman build+run against real
QEMU/vms.ko):

  - GREEN with production sys_lock.c: 11 suites passed, 0 failed.
  - RED after reverting sys$enq/sys$enqw to an always-succeeds stub (same
    defect shape as the sys_event.c regression): test_syssvc_lock's
    cross-process NOQUEUE-denial and post-release-grant assertions fail
    (10 suites passed, 1 failed) while every test_kmod_* stays green --
    proving the ioctl tests are structurally blind to this class of bug
    and the new test is not.
  - GREEN again after restoring sys_lock.c.

Also:
  - src/libvms/include/lksdef.h: promotes sys_lock.c's private LKSB
    struct to a public header (zero behavior change) -- external callers
    had no way to build the lksb parameter sys$enq/sys$enqw/sys$deq
    require.
  - tests/qemu/CMakeLists.txt: builds test_syssvc_lock against real
    /dev/vms when present; ctest SKIP (exit 77), never a fake PASS, when
    it is not (every dev/CI container -- Rule 9, Docker is not a runtime).
  - .github/workflows/ci.yml: updates the kernel-executive negative-
    control job's exact suite-count assertion (3 passed/7 failed -> 3
    passed/8 failed), empirically re-measured against a real
    NEGATIVE_CONTROL=1 build+run -- test_syssvc_lock also depends on
    /dev/vms and joins the "fails honestly when absent" bucket.

Found and flagged, not fixed (out of this item's scope): src/libvms/include/lckdef.h
duplicates starlet.h's LCK$M_* flag constants with DIFFERENT, stale
values (e.g. LCK$M_NOQUEUE 0x8 vs. starlet.h's oracle-pinned 0x4) --
see the file-header comment in test_syssvc_lock.c and this item's
returned findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rebased onto current origin/main (past vms-e4d) and fixed the three
merge-blockers an adversary found against round 2, all verified against
real podman build+QEMU runs (never just code-read):

B1 - THE CAPABILITY WAS NOT GENERAL. tests/qemu/Dockerfile named exactly
one binary twice (`--target test_syssvc_lock`, `cp .../test_syssvc_lock`),
so the next test_syssvc_*.c a Phase 1/3 item adds would build, ctest-SKIP
fine locally, and never reach the QEMU initramfs. Fixed:
  - tests/qemu/CMakeLists.txt now globs test_syssvc_*.c and registers each
    automatically via qemu_syssvc_add_test(), collecting every target name
    into a GLOBAL property and exposing a single `qemu_syssvc_tests` custom
    target that depends on all of them.
  - tests/qemu/Dockerfile builds `--target qemu_syssvc_tests` (not a named
    binary) and copies build-static/bin/test_syssvc_* by glob into the
    initramfs.
  - Proved generality empirically: added a throwaway test_syssvc_dummy.c
    with NO Dockerfile/CMakeLists.txt edit, podman-built, and confirmed it
    ran inside QEMU ("test_syssvc_dummy: 1 passed, 0 failed", FINAL RESULTS
    12/0). Removed the dummy and reran -- back to 11/0, matching the new
    ci.yml assertion below. init.sh's existing `/tests/test_syssvc_*` glob
    already handled the run side; only the build/copy side was hardcoded.
  - Added a suite-count assertion to the POSITIVE kernel-executive CI job
    (previously only the negative-control job pinned a count), so a test
    that silently stops being built/staged/run can no longer stay green.

B2 - THE NO-SILENT-FALLBACK PROOF WAS CIRCULAR. test_syssvc_lock bailed at
its own vms_kif_open() bootstrap and exited SKIP(77) before any sys$ call
was made, so sys_lock.c's SS$_NOSUCHDEV return path (do_enq/sys$deq) was
never actually exercised -- constraint #2 was satisfied by code reading.
Fixed: when bootstrap fails, the test now calls the PUBLIC sys$enqw and
sys$deq entry points directly (vms_kif_open() is idempotent on failure, so
this drives the real ensure_kif_open()-fails branch in sys_lock.c) and
CHECKs the returned status AND the LKSB's own status field both equal
SS$_NOSUCHDEV. A failed check now returns exit 1 (real FAIL), not a masked
77. Proved the gate can go red: injected a defect in do_enq() (fake
SS$_NORMAL success instead of SS$_NOSUCHDEV when /dev/vms is absent),
rebuilt+ran the negative-control image, watched the new assertions FAIL
("test_syssvc_lock: 1 passed, 2 failed"), then reverted and reran green.

B3 - lksdef.h WAS PRESENTED AS VMS-AUTHENTIC. Rewrote the header comment
per CLAUDE.md Rule 8: explicitly labeled an OVMX design choice, not a VMS-
published layout, citing the oracle finding that SYS$LIBRARY:STARLET.MLB
has no $LKSB macro at all (%LIBRAR-W-NOMTCHFOU) -- there is nothing
authentic to pin the byte layout against.

Also (low priority, honesty over fixing): documented in
tests/qemu/CMakeLists.txt that the ctest registration of test_syssvc_lock
SKIPs in 100% of environments where ctest runs, and is invoked directly by
init.sh (not through ctest) inside QEMU -- it buys build-graph inclusion
and an honest SKIP, not coverage, despite appearing in the ctest listing.

Verified via podman (docker is absent on this host; podman reproduces CI
exactly):
  - Positive job: FINAL RESULTS 11 suites passed, 0 suites failed.
  - Negative-control job: FINAL RESULTS 3 suites passed, 8 suites failed;
    new no-silent-fallback CHECKs all PASS (SS$_NOSUCHDEV asserted, not a
    string this program authored).
  - Injected-defect run: new CHECKs FAIL as expected, proving the gate can
    trip; reverted and reran green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e-count floor)

F1: Dockerfile, distro/Dockerfile.bootable, and tests/qemu/Dockerfile all
`COPY . <dest>` the full build context with no .dockerignore, so any local
build-*/ tree, .git history, or stray artifact in the working copy lands in
the image byte for byte -- slow, fat, and a disclosure risk. Reproduced from
this exact working copy (build-ci/, build-docker/, build-test/,
build-verify/, .git, docs/, tracking/, third-party/ all present on disk).
Added a repo-root .dockerignore excluding VCS/agent-state/historical-doc
directories verified (by grep across all CMakeLists.txt and all three
Dockerfiles) to be unread by any build step. Confirmed via `podman build`
+ exec that /src/repo no longer contains build-ci, .git, docs, tracking, or
third-party, and that all three Dockerfiles (root, bootable, qemu) still
build and smoke-test clean.

F2: the round-3 kernel-executive CI job hard-pinned
'11 suites passed, 0 suites failed', so the very next item that legitimately
adds test_syssvc_event.c would raise the true count to 12 and turn CI red
for succeeding -- defeating the glob-based generality vms-1d9 exists to
build. Replaced the exact pin with three checks that only go red when a
suite is REMOVED or FAILS, never when one is ADDED: zero-failures, a floor
of >=11 passed, and presence of every named suite's init.sh header line.

Proved both directions against real QEMU runs (not just regex review):
  - Removed tests/qemu/test_kmod_access.c, rebuilt, ran in QEMU for real:
    output showed '10 suites passed, 0 suites failed'; new check goes RED
    (floor check) and independently RED (named-suite check, verified with
    the count artificially padded back to 11 to isolate that layer).
  - Restored test_kmod_access.c; added a genuine throwaway
    test_syssvc_throwaway.c (deleted before this commit), rebuilt with zero
    Dockerfile/CMakeLists edits, ran in QEMU for real: output showed
    '12 suites passed, 0 suites failed'; new check stays GREEN.
  - Rebuilt the final tree (no throwaway file) and reran in QEMU: baseline
    '11 suites passed, 0 suites failed' unchanged, new check GREEN.
  - Reran the negative-control image (NEGATIVE_CONTROL=1): unchanged
    '3 suites passed, 8 suites failed', RC=1 -- untouched by this change.

Everything else the round-3->4 adversarial review found (SS$_NOSUCHDEV
2680 vs oracle 2312, lckdef.h bit-value drift, zero production callers of
vms_kif_register(), the QEMU gate's blindness to src/vmsdcl, only the lock
manager reachable) is pre-existing debt, already filed separately, and is
untouched here.

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

Round 4 fixed two findings and introduced four. This undoes that damage and
closes the one gap that was never gated. Every claim below is proven by a real
podman build + QEMU boot, not by inspection.

G1 .dockerignore no longer excludes CLAUDE.md. Round 4's exclusion broke this
   epic's OWN Rule 9 standing gate inside every image built from the repo root:
   tests/integration/test_runtime_target.sh greps CLAUDE.md, and
   `ctest -R runtime_target_gate` went red in the image while CI stayed green
   (CI only ran ctest on the host checkout) -- a silently broken guardrail.
   Now passes in-image.

G2 .dockerignore no longer excludes third-party/, which src/imgact/test/
   run_tcc_{native,rms,object_native,selfhost}.sh and src/vmslink/mk_tcc.sh
   hard-require. The whole list is narrowed to non-source state only, and it is
   now validated by RUNNING the suite inside the resulting image rather than by
   grepping the build files -- the method that missed both of the above.

G3 The suite gate is strong in both directions with nothing maintained by hand.
   Round 3 pinned an exact tally (red on a legitimate addition); round 4
   replaced it with a floor plus a hand-maintained name list whose own comment
   said it is not updated on addition, leaving every future suite unprotected.
   init.sh now prints a machine-readable per-suite verdict carrying the
   binary's real exit status, and CI derives the expected suite set from
   `ls tests/qemu/test_*.c`. Suite ADDED -> green; suite DROPPED -> red;
   suite FAILS -> red. A monotone floor on the number of suite SOURCES catches
   outright deletion, which a derived set cannot see.

G4 The negative-control job gets the same treatment. Its exact 3/8 tally pin
   turned red when a legitimate test_syssvc_*.c was added (proven: 3/9).

G5 The decisive one. A real silent fallback in sys_lock.c (SS$_NORMAL instead
   of SS$_NOSUCHDEV when /dev/vms is absent, in do_enq and sys$deq) left the
   FINAL RESULTS accounting BYTE-IDENTICAL -- 3 passed / 8 failed, RC 1 -- and
   every assertion in both jobs still passed. Cause: init.sh funnels exit 77
   (honest skip) and exit 1 (assertion failure) into one counter, and CI pinned
   only the total, so a per-process fake that reports success was invisible to
   the entire gate. The negative control now asserts that every test_syssvc_*
   suite exits exactly 77, which holds only when its device-absent
   SS$_NOSUCHDEV assertions all passed. Re-injecting that exact fallback now
   turns the job RED; reverting restores green (identical image SHA).

Unchanged: test_syssvc_lock.c, tests/qemu/CMakeLists.txt, sys_lock.c, lksdef.h.
The proven core -- a QEMU test linking the real libvms catching a userspace
defect all eight raw-ioctl suites miss -- was not touched, and was re-verified:
deleting one kstat_to_ss() line turns test_syssvc_lock red while all 8
test_kmod_* suites stay rc=0.

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

GitHub runs `run:` steps under `bash -e`, and `grep -c` exits 1 on an empty
set, so an empty derived suite list would have aborted the step with no
diagnostic instead of reaching the explicit source-count floor below it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Main moved six times under this branch. Resolutions, all deliberate:

 - tests/qemu/Dockerfile: keep main's libvmssys-linked devtab/procnam stage
   AND this branch's cmake/musl libvms stage; widen the source COPY back to
   test_*.c and dispatch build recipes BY PATTERN, never by literal name.
 - tests/qemu/init.sh: main was unchanged; keep the per-suite verdict line.
 - .github/workflows/ci.yml: drop main's exact tally pin ('3 suites passed,
   10 suites failed') in favour of per-suite verdicts derived from the
   checkout. The pin's comment argued the brittleness was the feature; it
   also turns the job red for a legitimately ADDED suite, which is the thing
   this item exists to make cheap for vms-ef1/vms-as1/vms-pv1.
 - src/libvms/syssvc/sys_lock.c: reverted to main. This branch's promotion
   of struct lksb to a public src/libvms/include/lksdef.h is DROPPED -- the
   LKSB has no VMS-published byte layout (no $LKSB macro exists in the
   oracle's STARLET.MLB), so per Rule 8 it is an OVMX design choice needing
   operator sign-off, and the item says not to absorb that here. The test
   declares its own LKSB storage, which is what OpenVMS callers do anyway.
 - tests/qemu/test_syssvc_lock.c: device-absent assertions no longer pin
   SS$_NOSUCHDEV. vms-0ff deleted that path as a fiction; pinning it would
   freeze a superseded contract into a gate. They now assert the property
   that survives the ruling -- no fabricated success.
 - tests/qemu/CMakeLists.txt: add_test() removed. It reported Skipped in
   100% of environments where ctest runs and was never invoked in the one
   environment where it can pass. Rule 10: a permanently-skipped test is a
   failing test.
…g (vms-1d9)

Round 6. Three blocking defects from the round-5 verdict, each closed and each
proven by its own MINIMAL mutation that trips that property and no other. All
runs are real podman builds + real QEMU boots on this host (aarch64, TCG, no
KVM); tools/replay_ci_kernel_executive.py executes the ci.yml assertion blocks
VERBATIM out of the YAML against the captured output, so a local proof cannot
drift from what CI runs.

1. THE GENERALIZATION IS NO LONGER FAKE.
   tests/qemu/Dockerfile dispatches BY PATTERN everywhere a name was once
   literal: `COPY tests/qemu/test_*.c` (was narrowed to test_kmod_*.c, so a
   future non-kmod source was dropped), a case-based build dispatch, the
   aggregate `--target qemu_syssvc_tests`, and a glob cp of every staged
   test_syssvc_* binary. A `|| exit 1` was added to the gcc loop -- without it
   a compile failure only broke that iteration and RUN exited with the LAST
   iteration's status, so a suite that stopped compiling vanished silently.
   The staging step now FAILS THE IMAGE BUILD if zero test_syssvc_* binaries
   were staged.
   PROOF: added a second suite (test_syssvc_evt.c) with ZERO Dockerfile edits
   -> built, staged, RUN ("=== SUITE test_syssvc_evt rc=0 ==="), positive job
   green at 13 derived suites, negative-control job green at 3/12. Then
   narrowed the cp back to the literal test_syssvc_lock: the harness still
   printed "ALL KERNEL MODULE TESTS PASSED" and exited 0, and the positive CI
   job went RED -- "test_syssvc_evt: NEVER RAN (no verdict line)". Throwaway
   suite removed.

2. THE POSITIVE JOB NOW HAS A GATE, NOT JUST AN EXIT CODE (vms-d2d).
   It derives the expected suite set from `ls tests/qemu/test_*.c` at CI time
   and asserts each suite's own "=== SUITE <name> rc= ===" verdict, plus a
   suite-count FLOOR of 12 for the case the derived set cannot see: a source
   deleted outright.
   PROOF: deleted tests/qemu/test_kmod_ast.c. Harness exit 0, "ALL KERNEL
   MODULE TESTS PASSED", 13 suites passed / 0 failed -- and the positive job
   went RED: "only 11 suite sources under tests/qemu (expected at least 12)".
   Restored.

3. THE NEGATIVE CONTROL NOW CALLS A PUBLIC sys$ ENTRY POINT AND JUDGES WHAT
   IT RETURNS, not the test's own printf.
   IMPORTANT SCOPE CORRECTION vs the dispatch: it does NOT assert
   SS$_NOSUCHDEV. vms-0ff ruled OVMX has no executive-absent state and DELETED
   sys_lock.c's per-call SS$_NOSUCHDEV returns; pinning that value would
   freeze a superseded contract into a gate -- the exact failure this epic's
   adversaries keep catching. What survives the ruling is a PROPERTY, not a
   VMS behaviour: a public sys$ entry point must never report SUCCESS when it
   did not reach the executive. The test asserts the odd/even success bit, an
   empty lock ID, and prints the raw status for the record; CI pins the
   suite's rc to exactly 77.
   PROOF: injected a fabricated success into do_enq and sys$deq (SS$_NORMAL +
   lock ID 0x1234 when the executive was unreachable). All four device-absent
   assertions FAILED, rc 77 -> 1, and the negative-control job went RED naming
   the cause -- while FINAL RESULTS stayed BYTE-IDENTICAL to the clean tree
   ("3 suites passed, 11 suites failed"), i.e. the tally pin this replaces
   would have stayed green. Same mutation left the POSITIVE job green (12/12),
   confirming it trips one property and not the others. Restored.

ALSO FIXED, found while proving #3: test_syssvc_lock could HANG the whole VM.
The child's post-release sys$enqw blocks in the kernel, and
src/kernel/vms_lock.c's enq_wait_sync re-arms on every signal wake without
returning to user mode -- so a child-side alarm(20) is swallowed. Measured: an
unreleased lock sat until run_tests.sh's 120s QEMU timeout, every later suite
never ran, and CI saw an unattributable timeout. The bound now lives in the
PARENT (poll-based read_bounded + WNOHANG reap), which is not blocked. With
sys$deq stubbed, the suite now fails in 20s with a named line and the harness
still reaches its own accounting (13 passed / 1 failed).

DELIBERATELY NOT DONE, and why:
 - tests/qemu/CMakeLists.txt's add_test() is REMOVED. It reported Skipped in
   100% of environments where ctest runs and was never invoked in the one
   environment where it can pass (init.sh execs the binary directly), so it
   never executed as a passing assertion anywhere. Rule 10: a permanently-
   skipped test is a failing test, and a comment does not discharge it. No
   coverage is lost -- add_executable keeps it in the default `all` target, so
   it still breaks the host build if it stops compiling, and the QEMU job runs
   and gates it. Host ctest: 40 tests, 40 passed, 0 skipped.
 - src/libvms/include/lksdef.h is DROPPED and sys_lock.c is untouched. The
   LKSB has no VMS-published byte layout (the oracle's STARLET.MLB has no
   $LKSB macro), so per Rule 8 a shared header is an OVMX design choice
   needing operator sign-off. The test declares its own LKSB storage, which is
   what OpenVMS callers do anyway.
 - SS$_NOSUCHDEV 2680-vs-2312, lckdef.h's nine wrong flag bits: untouched,
   separately tracked.
@baron-3dl
baron-3dl merged commit 250e188 into main Jul 30, 2026
34 checks passed
baron-3dl added a commit that referenced this pull request Jul 30, 2026
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 added a commit that referenced this pull request Jul 30, 2026
…s, so every /dev/vms ioctl failed (vms-9fc) (#16)

* Bind the product to the executive: vms_kif_register() had zero callers (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>

* Open /dev/vms unconditionally in kif_bind, and re-anchor the Rule 9 controls (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>

* One PCB per PROCESS, not per Linux thread (vms-9fc)

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>

* Finish the SS$_ILLIOFUNC correction: F$MESSAGE could not name it (vms-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>

* A negative control that silently no-ops must fail loudly (vms-9fc)

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>

* Record how the negative-control floor was recomputed (vms-9fc)

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>

---------

Co-authored-by: Claude Opus 5 (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