Skip to content

Terminals are devices in an executive device table, not a pty wearing a VMS name (vms-d0b) - #13

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

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

Conversation

@baron-3dl

Copy link
Copy Markdown
Contributor

Second of the three Rule 11 facades replaced with a real executive facility. Partial by design — see What this does not do.

What landed

An executive-resident device table in vms.ko (src/kernel/vms_devtab.c), with $ASSIGN / $DASSGN / $GETDVI / $ALLOC / $DALLOC / device scan / terminal setmode behind /dev/vms (ioctls 0x50–0x56; the process table from vms-8019 holds 0x40–0x43, disjoint). The console is a real device with a VMS name, an owner, a reference count and characteristics that two processes agree on.

Ownership semantics: measured, not inferred — and the first answer was wrong

Earlier rounds asserted "ownership comes from $ALLOC, never from $ASSIGN" and labelled it oracle-measured in four places, plus a rule that a foreign channel-holder causes SS$_DEVALLOC, labelled oracle-measured in three more. Adversarial review found the capture didn't show either. The refusal it rested on happened while the device sat at its idle baseline, on a device SHOW DEVICE/FULL calls shareable — a far likelier reason.

So the controlled experiment was run on the live VAX 7.3 lab, and the real rule is neither of the above:

observation result
OPEN/WRITE TTA0: (channel only, no ALLOCATE), non-shareable terminal Owner "SYSTEM", refcount 1, no allocated in the status clause
identical OPEN on NLA0:, "shareable, mailbox device" Owner "" — ownership never moves
detached CHANHOLD holding one channel; second process ALLOCATE TTA0: %SYSTEM-W-DEVALLOC
STOP CHANHOLD Owner "", refcount 0

Ownership comes from holding a channel to a non-shareable device. Shareability is the criterion, not allocation and not login. OVMX now matches VMS (Rule 10 answer 1) rather than hiding. The withdrawn inference is marked WITHDRAWN in docs/oracle/vax73-terminal-device.md rather than quietly rewritten, and states plainly that why ALLOCATE NLA0: fails remains unexplained and OVMX models nothing on it. %SYSTEM-W-DEVASSIGN (2120) is known only by its message text, never provoked — so nothing returns it (vms-6bd).

Verification

Independently reproduced by an adversarial reviewer from a clean git archive, on real aarch64 QEMU under TCG:

  • Positive: test_kmod_devtab 60/60 and test_kmod_procnam 30/30 against the same loaded vms.ko — 12 suites, 0 failed.
  • Negative control: 3 passed / 9 failed, both suites halting honestly at cannot open /dev/vms (executive absent) with no per-process fallback. ci.yml's pinned literal moved 8 → 9 and matches byte-for-byte.
  • Facade mutation: the reviewer patched devinfo_fill() to return a per-process view — restoring the exact facade this item kills — and the suite went 48 passed / 12 FAILED. Every A-writes/B-reads assertion fired. The test discriminates.
  • Host ctest 39/39, 0 failed, 0 skipped.

Device-table struct layouts and ioctl encodings are now frozen by _Static_assert the way vms-8019 froze the process table's, mutation-tested to confirm they fire.

What this does not do

SHOW DEVICE / SHOW TERMINAL are not yet readers of this table, and $QIO to a terminal is not implemented. Those are blocked on vms-1d9 (the QEMU harness cannot yet build DCL) — tracked as vms-fb9. The VMS_TERMINAL / VMS_DEVICE_TYPE environment handoff is therefore still in place and is marked loudly as a stopgap. vms-d0b stays open.

Known gaps filed rather than hidden: vms-99ce (no shareable device exists to assert the shareable arm), vms-23b (unmeasured: does a surviving channel-holder inherit ownership), vms-0d7 (pre-existing: vmsfs_device.c:52 returns SS$_DEVALLOC for a full table).

🤖 Generated with Claude Code

baron-3dl and others added 9 commits July 30, 2026 09:13
A VMS device is a thing the executive knows about. The driver enters a
unit in the I/O database at boot and from then on the device exists for
every process on the node: $ASSIGN takes a channel to it, $GETDVI reads
it, $DEVICE_SCAN enumerates it, and SHOW DEVICE / SHOW TERMINAL are
readers of that one table. Ownership, reference count and terminal
characteristics are properties of the DEVICE, not of whoever is asking.
OVMX had none of that -- terminal identity was a VMS_TERMINAL
environment variable, which is the rejected VMS_PRCNAM shape (rule 10)
and a facade by rule 11's definition.

This adds the table to the executive:

  src/kernel/vms_devtab.c   device table, channels, $ASSIGN/$DASSGN,
                            $GETDVI, $DEVICE_SCAN, IO$_SETMODE
  src/kernel/vms_ioctl.h    shared ABI + the VMS_TTC_* characteristics
  src/libvmssys/vms_kif.c   userspace client
  tests/qemu/test_kmod_devtab.c  A-writes / B-reads against /dev/vms

The console OPA0: is created by the executive at module init -- no
process registers it, and a process that never asked for it still sees
it. Ownership is taken by the first channel to an unowned device and
released when that process's last channel goes, including when the
process dies; the device itself outlives it.

Terminal characteristics are set THROUGH A CHANNEL, as VMS sets them
($QIO IO$_SETMODE). A caller with no channel gets SS$_IVCHAN, so a
process cannot redefine a terminal it never opened.

Oracle-pinned (rule 10) from the ~/vax OpenVMS VAX V7.3 lab, captured
verbatim in docs/oracle/vax73-terminal-device.md: the console name
OPA0:, the characteristic names and their two-state form, "Device_Type:
Unknown" for an unidentified terminal, and %SYSTEM-W-NOSUCHDEV for an
absent device. What the oracle prints but OVMX cannot honestly answer --
line speed, parity, fill characters -- is NOT carried in the device
record at all rather than being given a plausible value. The
characteristic BIT POSITIONS are an OVMX design choice and are labelled
as such: the public docs do not publish the $TTDEF layout.

Proven, not asserted: tests/qemu/test_kmod_devtab.c runs against a real
/dev/vms inside QEMU. Process A assigns OPA0:, takes ownership and
changes width, page and characteristics; process B, which did none of
that, reads every change back. 33/33 with the module loaded; with
NEGATIVE_CONTROL=1 (vms.ko not loaded) the same test fails honestly at
"cannot open /dev/vms" -- there is no per-process fallback to fall back
to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The item's condition for leaving the VMS_TERMINAL / VMS_DEVICE_TYPE
handoff in place is that it must be LOUD about being a stopgap rather
than silent. Comments only -- no behaviour change, no test change.

Four sites, each now saying what it is, why it is still here, and what
replaces it:

  src/ovmx_init/ovmx_init.c   setenv("VMS_TERMINAL", "_OPA0:")
  src/vmsdcl/dcl_main.c       getenv(VMS_TERMINAL/VMS_DEVICE_TYPE) plus
                              the private _FTA allocation pool
  src/vmsdcl/dcl_cmd_show.c   SHOW TERMINAL prints the DCL context's own
                              copy; SHOW DEVICE walks /proc/mounts and
                              prints a hardcoded row when it finds none
  src/vmsssh/vmssshd.c        the same handoff for remote sessions
                              (out of scope for vms-d0b: marked, not fixed)

The replacement in each case is $ASSIGN to the device and $GETDVI /
$DEVICE_SCAN on the executive's table, which now exists and is proven.
What blocks the switch is that DCL is not built into the runtime where
/dev/vms exists -- recorded in the vms-d0b escalation, not papered over
here with a fallback.

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

The first cut asserted an ownership rule as VMS fact with nothing behind
it: "the first channel to an unowned device makes its holder the owner",
and a test that certified it. Neither half survived contact with the
oracle. Measured on the ~/vax OpenVMS VAX V7.3 lab (node VAX2), from a
second process created with RUN/DETACHED, issuing $ASSIGN through a
MACRO-32 program so VMS printed its own message:

  $ASSIGN OPA0: from a process that does not own the console
      -> %SYSTEM-S-NORMAL
  ALLOCATE OPA0: from that same process, seconds later
      -> %SYSTEM-W-DEVALLOC, device already allocated to another user

and, holding an open channel to NLA0: throughout:

      Owner process ""  Owner process ID 00000000  Reference count 2 -> 3 -> 2

So a channel confers no ownership at all, ownership is $ALLOC's, and
SS$_DEVALLOC belongs to $ALLOC rather than $ASSIGN. A terminal owned by
another process is assignable but not allocatable.

What changes:
 - $ASSIGN no longer touches ownership. It adds a reference and a
   channel, and succeeds on a device somebody else owns.
 - $ALLOC / $DALLOC (ioctls 0x55/0x56, vms_kif_alloc / vms_kif_dalloc)
   are what set and clear the owner. Every branch is a case observed on
   the lab: allocated elsewhere -> SS$_DEVALLOC; another process merely
   holding channels -> SS$_DEVALLOC (ALLOCATE NLA0: is refused with the
   owner field still empty); already ours -> succeeds and changes
   nothing, reference count included; $DALLOC of something we do not
   have -> SS$_DEVNOTALLOC.
 - A device carries `allocated` separately from `owner_pid`, because the
   oracle prints them separately -- ALLOCATE adds the word "allocated"
   to SHOW DEVICE/FULL's status clause and one to the reference count.
 - A dying process loses its allocations as well as its channels. A
   device allocated to a process that no longer exists is not a state
   VMS has.

SS$_DEVALLOC was 2316 in ssdef.h. The same probe shows 2316 is
%SYSTEM-F-NOSUCHDEV; the real value is 2112, with SS$_DEVNOTALLOC at
2136. Both corrected here with the citation, because this is the change
that introduces their first consumer. The probe also disagrees with
SS$_IVCHAN, SS$_IVDEVNAM and SS$_NOSUCHDEV, which have consumers across
the kernel module and its tests -- filed as vms-47f, not touched here.

Deliberately NOT modelled (rule 10 -- do not invent a handler for a
condition that cannot be pinned): the lab's OPA0: carries a device
protection mask and the probe ran as SYSTEM, so it pins the allocated
case and says nothing about an unprivileged one. OVMX has no device
protection to check and does not pretend to.

Proof: tests/qemu/test_kmod_devtab.c, 48 assertions against a real
/dev/vms inside QEMU (aarch64, podman, TCG, no KVM) -- 48/48, whole
suite 11/11, exit 0. NEGATIVE_CONTROL=1 (vms.ko never insmod'ed): the
same test fails honestly at "cannot open /dev/vms", 8 suites red, exit
1. Host ctest unchanged at 40/40 pass, 1 skip.

Oracle transcripts: docs/oracle/vax73-terminal-device.md sections 7-9.
The header said "Terminal characteristic bits (matching VMS TT$
constants)". They do not match. Neither the bit positions nor, in
several cases, the NAMES correspond to OpenVMS: dcl_terminal.c's
char_display[] carries Scope, Holdscreen, Mechtab, Oper, Page, Runout
and AltTypeAhd, none of which appear in OpenVMS VAX V7.3's SHOW TERMINAL
output, and it omits most of the names that do.

Rule 8 lets OVMX define its own representation where the public
documentation publishes no byte-level layout -- true of $TTDEF -- but
requires it to be LABELLED as an OVMX design choice rather than
presented as VMS-authentic. This is that label, in the same words used
for the executive's channel-number policy and its own TTC vector.

The divergence itself is user-visible SET/SHOW TERMINAL output, not a
comment, so it is filed as vms-2cb rather than fixed here (rule 7: a
deviation gets an item, not a note). The oracle's real V7.3 name list is
docs/oracle/vax73-terminal-device.md section 2.
…n-shareable device owns it

Round 3 wrote an inference into the kernel as a measurement: "another
process merely holds channels -> SS$_DEVALLOC", cited as oracle-pinned in
three places. The capture behind it did not show that. ALLOCATE NLA0: was
refused while NLA0: sat at its idle baseline (Owner "", reference count 2)
and NLA0: is a "shareable, mailbox device" -- being shareable is a far
likelier reason it was refused, and nothing established that those two
references belonged to other processes.

So the experiment the inference needed was run, on the live ~/vax OpenVMS
VAX V7.3 lab (node VAX2). Verbatim, /tmp/clean-vax1-test/vax2.log:

  TTA0:, a terminal -- its status clause carries no "shareable":

    line 1121  Owner process ""            line 1123  Reference count 0
    line 1127  $ OPEN/WRITE X TTA0:                    (a channel, nothing else)
    line 1136  Owner process "SYSTEM"      Owner process ID 20400216
    line 1138  Reference count 1
    line 1132  "is online, record-oriented device, carriage control"  <- no "allocated"
    line 1142  $ DEALLOCATE TTA0:
    line 1143  %SYSTEM-W-DEVNOTALLOC, device not allocated
    line 1158  $ CLOSE X
    line 1165  Owner process ""            line 1167  Reference count 0

  NLA0:, "shareable, mailbox device" (line 1176), same DCL sequence:

    line 1179  Owner process ""            line 1181  Reference count 3

An earlier probe in the same log reached the same conclusion from the other
direction: CHANHOLD, a detached process running a MACRO-32 image whose only
act was $ASSIGN_S to TTA0: followed by $HIBER_S, showed as

    line 1005  Owner process "CHANHOLD"    Owner process ID 20400218
    line 1007  Reference count 1                        (no "allocated")
    line 1009  $ ALLOCATE TTA0:
    line 1010  %SYSTEM-W-DEVALLOC, device already allocated to another user

and STOP CHANHOLD put the device back to Owner "" / reference count 0
(lines 1036-1038).

WHAT CHANGES:

 - $ASSIGN to a NON-SHAREABLE device that nobody owns makes the caller the
   owner, with no allocation and no extra reference. Devices carry a
   `shareable` flag for exactly this, mirroring the word the oracle prints;
   the console is not shareable.
 - The foreign-channel walk in $ALLOC is DELETED. It is unnecessary as well
   as unmeasured: a foreign channel to a non-shareable device makes that
   process the OWNER, so the single measured refusal -- "somebody else owns
   it" -- covers both observed cases with one rule.
 - Ownership that rests only on a channel ends when the owner returns its
   last channel, or dies. An allocation outlives the channel: DEALLOCATE
   OPA0: took the reference count 3 -> 2 and dropped the word "allocated"
   while leaving the still-channel-holding job as Owner "SYSTEM"
   (log lines 670-695).
 - $DALLOC while owning only by channel is SS$_DEVNOTALLOC, and leaves the
   ownership alone.

STILL NOT MODELLED, deliberately: %SYSTEM-W-DEVASSIGN, "device has channels
assigned" (2120). It is known only from an F$MESSAGE lookup (log line 698),
never observed as the answer to any operation, so nothing returns it.

Also folded in: the UIC derivation moves to one helper, still explicitly
NOT oracle-pinned.

vms-d0b
…oven alone

The round-3 suite could not tell its own two refusals apart. An adversary
proved by mutation that deleting the foreign-channel refusal ALONE left it
48/48 green, and deleting the "allocated by another" refusal ALONE also left
it 48/48 green; only deleting both went red. Neither rule was proven,
including the one that was genuinely measured. The cause was the test's
shape: process A assigned and allocated in one breath, so every refusal had
two possible reasons and either one sufficed.

A now runs in two phases, synchronised on a pipe -- B's word, never a timer:

  phase 1  A holds NOTHING BUT A CHANNEL. B observes that A is already the
           owner and that nothing is allocated, and gets SS$_DEVALLOC from
           $ALLOC and SS$_DEVNOTALLOC from $DALLOC in that state. This is
           the oracle's CHANHOLD case (vax2.log l.1005-1010) and it is the
           case no assertion covered before.
  phase 2  A allocates. B re-runs the refusals against a genuinely
           allocated device (the oracle's detached-ALLOCATE case, l.548).

Section 5 now has B return its own channel BEFORE killing A, so that when A
dies nobody else is holding the console: whether a surviving channel-holder
inherits ownership was never measured and this test asserts no answer to it.

Section 6 walks the whole life cycle in one process in the order the oracle
showed it: channel -> owned but unallocated (TTA0: l.1136-1138);
$ALLOC -> allocated, +1 reference (OPA0: 2 -> 3, l.682); $DALLOC -> the
allocation and its reference go but OWNERSHIP STAYS, because we still hold a
channel (OPA0: still Owner "SYSTEM" at l.693); last channel back -> unowned
(TTA0: CLOSE, l.1165).

Every assertion that says "oracle" now carries the log line it came from.

RUN, not described: podman build -f tests/qemu/Dockerfile, then podman run,
on this aarch64 host under QEMU TCG with no /dev/kvm --
  === test_kmod_devtab: 60 passed, 0 failed ===
  === FINAL RESULTS: 11 suites passed, 0 suites failed ===
exit 0. Assertions went 48 -> 60; none was removed or weakened.

vms-d0b
… say what replaced it

The document asserted, as section 7.4, that ALLOCATE NLA0: -> DEVALLOC proved
that channels held by other processes refuse an allocation. It proves no such
thing: NLA0: was at its idle baseline (Owner "", reference count 2, the same 2
it had before and after the observer's own OPEN/CLOSE), nothing established
that those references were foreign, and NLA0: is a "shareable, mailbox device"
-- a likelier reason ALLOCATE refused it. That section is now marked WITHDRAWN
rather than quietly rewritten, because the next reader needs to know a claim
was retracted, not just that the text changed.

In its place, measured on a NON-shareable device and cited line by line to
/tmp/clean-vax1-test/vax2.log:

  7.3  a channel to a SHAREABLE device confers nothing        (NLA0:, l.562-592)
  7.4  a channel to a NON-SHAREABLE device makes the assigner the OWNER,
       unallocated, at no extra reference, and returning it unowns the device
                                                              (TTA0:, l.1115-1167)
  7.5  $ALLOC is refused while another process owns the device by channel
       alone, and ownership dies with its owner               (CHANHOLD, l.979-1038)
  7.6  the withdrawal notice

Section 8 gains the fourth fact its own capture always contained and the text
had skipped: DEALLOCATE OPA0: left the still-channel-holding job as
Owner "SYSTEM" (l.689-695). Allocation and ownership come apart there too.

The mapping table is rewritten row by row against the code that now exists,
including the rows for release-on-last-channel and release-on-death.

Two honesty fixes while in here:

 - ssdef.h's provenance note claimed SS$_DEVALLOC "had no other consumer to
   break". It has two, dcl_cmd_misc.c and vmsfs_device.c. Both name the symbol
   so nothing breaks, but the sentence was false and is corrected. It also now
   records that vmsfs_device.c returns SS$_DEVALLOC for a full device table --
   the wrong condition entirely, pre-existing, carried in findings, not fixed
   here.
 - The shareable = 1 side of the ownership rule has no device in the table to
   exercise it and no test asserting it. That is now stated at the point of
   creation instead of being left for a reader to discover.

vms-d0b
main gained the executive-resident process table (vms-8019, 00f426c), which
touches the same seven files as this branch's device table. The conflicts are
textual -- two feature blocks appended to the same files -- not semantic: the
ioctl numbers are disjoint (0x40-0x43 process table, 0x50-0x56 device table),
so both families are kept whole and neither is renumbered.

Resolution, file by file:
 - src/kernel/Makefile: vms-y builds BOTH vms_devtab.o and vms_proctab.o.
 - src/kernel/vms_internal.h: both ioctl handler declaration blocks kept.
 - src/kernel/vms_ioctl.h: both feature sections kept, in ioctl-number order;
   vms-8019's _Static_asserts on struct sizes and ioctl encodings survive.
 - src/kernel/vms_module.c: BOTH arms of the ioctl dispatch switch kept, and
   in the process-teardown path both vms_proc_release_channels() (devtab) and
   the put_pid() of the pinned pid reference (proctab) run. Channels are given
   back first; the device table is not keyed on the pid reference, so the order
   is independent, but releasing the process's claims before dropping its
   identity is the readable order.
 - src/libvmssys/vms_kif.{c,h}: both client sections kept. The two sides
   interleaved badly here because both end in an identically-shaped
   ioctl-then-return-args.status tail, so the file was reassembled from each
   side's own version rather than from the conflict hunks: lines 1-379 are
   byte-identical on both sides, and each side's feature block is
   self-contained after that.
 - tests/qemu/Dockerfile: both test_kmod_devtab and test_kmod_procnam are
   excluded from the generic build loop and built by the explicit rule that
   links src/libvmssys/vms_kif.c, now a loop over both names so neither can be
   silently dropped.

.github/workflows/ci.yml: the executive-absent negative control is bumped
3 passed / 8 failed -> 3 passed / 9 failed. The merged tree has 12 suites;
test_kmod_devtab is the 12th and it depends on /dev/vms, so it must fail when
the executive is absent. Per the maintenance convention in that block the exact
literal is edited rather than softened to a range. Note this also corrects a
stale literal on this branch: work/vms-d0b still said 7 while the branch's own
measured negative control was 8, because the count was never bumped when
test_kmod_devtab was added.
… the process table's

The kernel module and the userspace client compile these structures
separately from one header and then pass them across /dev/vms by raw
address. vms-8019 pinned its process-table structs and ioctl encodings
with _Static_asserts for that reason; the device-table structs landed
without the equivalent, so a reorder or a widened field on one side
would have been a silent runtime mis-decode instead of a build failure.

Sizes and encodings are measured, not chosen. Because _IOWR folds
sizeof(struct) into the request number, a size change also renumbers
the request, so both assertions are worth having: one catches the
mis-decode, the other catches the -ENOTTY it would turn into.

Verified to actually fire: adding a uint64_t to struct vms_assign_args
fails the build on both its size assert and its encoding assert.
@baron-3dl
baron-3dl merged commit fcf2b80 into main Jul 30, 2026
34 checks passed
baron-3dl added a commit that referenced this pull request Jul 30, 2026
main advanced again mid-round to fcf2b80 (vms-d0b, PR #13).
src/ovmx_init/ovmx_init.c and src/libvmssys/vms_kif.c auto-merged clean --
executive_attach() still opens /dev/vms, halts inside the open-failure branch
and never closes the descriptor, and no vms_dev_fd absence guard came back.

The ci.yml negative-control literal is again the only conflict, and again the
textual merge of it was WRONG. vms-d0b moved it 8 -> 9 by adding
test_kmod_devtab; this branch already read 9 for a DIFFERENT reason (vms-8019's
test_kmod_procnam plus this branch's test_kmod_pin). With all three suites
present the real split is 3 passed / 10 failed, established by running the
negative-control harness on the merged tree rather than by reasoning about it.
The comment now says outright that this line must be RECOMPUTED from the tree on
every merge and never taken from either side of a conflict.
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