Terminals are devices in an executive device table, not a pty wearing a VMS name (vms-d0b) - #13
Merged
Merged
Conversation
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
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.
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.
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 fromvms-8019holds 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 causesSS$_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 deviceSHOW DEVICE/FULLcalls 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:
OPEN/WRITE TTA0:(channel only, noALLOCATE), non-shareable terminalOwner "SYSTEM", refcount 1, noallocatedin the status clauseOPENonNLA0:, "shareable, mailbox device"Owner ""— ownership never movesCHANHOLDholding one channel; second processALLOCATE TTA0:%SYSTEM-W-DEVALLOCSTOP CHANHOLDOwner "", refcount 0Ownership 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.mdrather than quietly rewritten, and states plainly that whyALLOCATE 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:test_kmod_devtab60/60 andtest_kmod_procnam30/30 against the same loadedvms.ko— 12 suites, 0 failed.cannot open /dev/vms (executive absent)with no per-process fallback.ci.yml's pinned literal moved 8 → 9 and matches byte-for-byte.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.ctest39/39, 0 failed, 0 skipped.Device-table struct layouts and ioctl encodings are now frozen by
_Static_assertthe wayvms-8019froze the process table's, mutation-tested to confirm they fire.What this does not do
SHOW DEVICE/SHOW TERMINALare not yet readers of this table, and$QIOto a terminal is not implemented. Those are blocked onvms-1d9(the QEMU harness cannot yet build DCL) — tracked asvms-fb9. TheVMS_TERMINAL/VMS_DEVICE_TYPEenvironment handoff is therefore still in place and is marked loudly as a stopgap.vms-d0bstays 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:52returnsSS$_DEVALLOCfor a full table).🤖 Generated with Claude Code