From 5ff0cc2ca66640367d6ed29cc891c4efeea49756 Mon Sep 17 00:00:00 2001 From: Chris Baron Date: Thu, 30 Jul 2026 09:13:35 -0400 Subject: [PATCH 1/8] vms-d0b: terminals are devices in an executive device table 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) --- docs/oracle/vax73-terminal-device.md | 166 ++++++++ src/kernel/Makefile | 3 +- src/kernel/vms_devtab.c | 570 +++++++++++++++++++++++++++ src/kernel/vms_internal.h | 83 ++++ src/kernel/vms_ioctl.h | 186 +++++++++ src/kernel/vms_module.c | 44 +++ src/libvmssys/vms_kif.c | 122 ++++++ src/libvmssys/vms_kif.h | 37 ++ tests/qemu/Dockerfile | 16 + tests/qemu/test_kmod_devtab.c | 305 ++++++++++++++ 10 files changed, 1531 insertions(+), 1 deletion(-) create mode 100644 docs/oracle/vax73-terminal-device.md create mode 100644 src/kernel/vms_devtab.c create mode 100644 tests/qemu/test_kmod_devtab.c diff --git a/docs/oracle/vax73-terminal-device.md b/docs/oracle/vax73-terminal-device.md new file mode 100644 index 000000000..67bb54f4a --- /dev/null +++ b/docs/oracle/vax73-terminal-device.md @@ -0,0 +1,166 @@ +# Oracle capture: the console terminal as a VMS device (OpenVMS VAX V7.3) + +**Item:** `vms-d0b` · **Captured:** 30-JUL-2026 · **Oracle:** the `~/vax/cluster` reference lab, +OpenVMS VAX V7.3 on SIMH, nodes **VAX1** (system id 1025) and **VAX2** (1026), driven over the SIMH +console by `nodedrv.py` (transcripts `/tmp/clean-vax1-test/vax1.log`, `vax2.log`). + +**Method (CLAUDE.md rule 8 — clean room).** Everything below is *observed behaviour of the running +system through its documented operator interface* (DCL `SHOW TERMINAL`, `SHOW DEVICE`, +`SET TERMINAL`). No VSI/HPE binary was disassembled, decompiled or read. Nothing here is a byte +layout: these are the names, the formats and the values VMS itself prints. + +**Why it exists.** OVMX's executive device table (`src/kernel/vms_devtab.c`) has to answer questions +about a terminal — its name, its class, its owner, its characteristics — and rule 10 gives exactly +two legal ways to answer: match VMS, or do not have the thing at all. This file is the "match VMS" +side of that: the record of what VMS actually says, so no later reader has to invent it. + +--- + +## 1. The console terminal is `OPA0:` + +`SHOW TERMINAL` on the console of both lab nodes prints the physical device name with the leading +underscore: + +``` +Terminal: _OPA0: Device_Type: LA36 Owner: SYSTEM +``` + +## 2. `SHOW TERMINAL`, verbatim — pristine console (VAX2, never had `SET TERMINAL` issued) + +``` +Terminal: _OPA0: Device_Type: LA36 Owner: SYSTEM + + Input: 300 LFfill: 0 Width: 132 Parity: None + Output: 300 CRfill: 0 Page: 24 + +Terminal Characteristics: + Interactive Echo Type_ahead No Escape + No Hostsync TTsync Lowercase No Tab + Wrap Hardcopy No Remote No Eightbit + Broadcast No Readsync No Form Fulldup + No Modem No Local_echo No Autobaud No Hangup + No Brdcstmbx No DMA No Altypeahd Set_speed + No Commsync Line Editing Insert editing No Fallback + No Dialup No Secure server No Disconnect No Pasthru + No Syspassword No SIXEL Graphics No Soft Characters No Printer Port + Numeric Keypad No ANSI_CRT No Regis No Block_mode + No Advanced_video No Edit_mode No DEC_CRT No DEC_CRT2 + No DEC_CRT3 No DEC_CRT4 No DEC_CRT5 No Ansi_Color + VMS Style Input +``` + +Notes taken from this, not inferred: + +- The characteristic list is **two-state per name**, printed in four columns, with the inactive form + spelled `No ` — except `Lowercase`/`Uppercase`, `Fulldup`/`Halfdup`, and `Insert editing`, + where the pair is a different word. `Interactive` is printed first and has no `No` form here. +- There is **no `Scope` characteristic** in the V7.3 list. `Hardcopy` is the axis. +- The header carries three fields (`Terminal:`, `Device_Type:`, `Owner:`); the second block carries + `Input:`/`Output:` speed, `LFfill:`/`CRfill:`, `Width:`, `Page:`, `Parity:`. + +**The in-tree `src/vmsdcl/dcl_terminal.c` `char_display[]` table does not match this list.** It +invents `Scope`, `Holdscreen`, `Mechtab`, `Oper`, `Page`, `Runout`, `AltTypeAhd` and omits most of +the names above. That is tracked as follow-up work; the table above is the oracle. + +## 3. Device type + +The console reports `Device_Type: LA36` on the lab because the VAX console *is* an LA36 hardcopy +terminal. An unidentified terminal is displayed as **`Unknown`** (capital U), observed by setting it +and reading it back on VAX1: + +``` +$ SET TERMINAL/DEVICE_TYPE=UNKNOWN +$ SHOW TERMINAL +Terminal: _OPA0: Device_Type: Unknown Owner: SYSTEM +``` + +With the device type Unknown, the characteristics VMS reports **set** are: + +``` +Interactive Echo Type_ahead TTsync Lowercase Wrap Hardcopy Broadcast +Fulldup Set_speed Insert editing Numeric Keypad VMS Style Input +``` + +(everything else `No ...`; notably `No Line Editing`, which V7.3 clears when the type becomes +Unknown, and `Hardcopy`, which persists because it describes the physical port). The device type +also redefaults geometry: switching back to `LA36` moved `Page` from 24 to 66. + +VMS's own login procedure fails to identify a console of this kind, and says so: + +``` +%SET-W-NOTSET, error modifying OPA0: +-SET-I-UNKTERM, unknown terminal type +``` + +## 4. `SHOW DEVICE` for a terminal + +``` +$ SHOW DEVICE OPA0: + +Device Device Error + Name Status Count +OPA0: Online 0 +``` + +`SHOW DEVICE TT` (class prefix) returns the same single row on these nodes. + +## 5. `SHOW DEVICE/FULL` for a terminal + +VAX1 (its console is enabled as an operator terminal): + +``` +$ SHOW DEVICE/FULL OPA0: + +Terminal OPA0:, device type LA36, is online, enabled as operator terminal, + record-oriented device, carriage control. + + Error count 0 Operations completed 373 + Owner process "SYSTEM" Owner UIC [SYSTEM] + Owner process ID 2020021A Dev Prot S:RWPL,O:RWPL,G,W + Reference count 2 Default buffer size 132 +``` + +VAX2 (not an operator terminal — the clause is simply absent, not replaced): + +``` +Terminal OPA0:, device type LA36, is online, record-oriented device, carriage + control. +``` + +So the device carries, and a reader may print: error count, operations completed, owner process +*name*, owner UIC, owner process ID, device protection, reference count, default buffer size. + +## 6. A device that does not exist + +``` +$ SHOW DEVICE ZZA0: +%SYSTEM-W-NOSUCHDEV, no such device available +``` + +--- + +## What OVMX took from this, and what it deliberately did not + +| Oracle fact | OVMX (`src/kernel/vms_devtab.c`) | +|---|---| +| Console is `OPA0:` | Executive creates `OPA0:` at module init | +| Terminal is device class terminal | `DC$_TERM` (6), mirroring `src/libvms/include/dcdef.h` | +| Unidentified type displays `Unknown` | Console registers with device type 0 = Unknown | +| Characteristic **names** and their two-state form | `VMS_TTC_*` in `src/kernel/vms_ioctl.h`, one bit per oracle name | +| Absent device → `%SYSTEM-W-NOSUCHDEV` | `SS$_NOSUCHDEV` from `$ASSIGN`/`$GETDVI` | +| Owner / reference count are device properties | `owner_pid` / `refcnt` in the executive, released when the owner's last channel goes | + +Deliberately **not** taken: + +- **Characteristic bit positions.** The public documentation available to this work does not publish + the `$TTDEF` byte layout, so OVMX defines its own vector and labels it as its own (rule 8). Only + the names are VMS's. +- **`Hardcopy` on the OVMX console.** It is set on the lab because that console is a printing + terminal. OVMX's console is not, so claiming it would be a false statement about our hardware. +- **`Input:`/`Output:` speed, `Parity`, `LFfill`/`CRfill`.** OVMX's console is a serial line with no + such parameters to report, so the executive carries no value for them and no reader can print one + (rule 10: hide it rather than report a plausible number). +- **`Width: 132` / `Page: 24`** were taken from the pristine VAX2 console, but with the caveat that + that console is a 132-column LA36. This is the one pair of constants in the device table that is + an analogy rather than a measurement of OVMX's own hardware, and it is flagged for operator + sign-off. diff --git a/src/kernel/Makefile b/src/kernel/Makefile index ae24bd9da..acab3012e 100644 --- a/src/kernel/Makefile +++ b/src/kernel/Makefile @@ -15,7 +15,8 @@ vms-y := vms_module.o \ vms_access.o \ vms_ast.o \ vms_eflag.o \ - vms_lock.o + vms_lock.o \ + vms_devtab.o ccflags-y := -I$(src) diff --git a/src/kernel/vms_devtab.c b/src/kernel/vms_devtab.c new file mode 100644 index 000000000..799af5be1 --- /dev/null +++ b/src/kernel/vms_devtab.c @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * vms_devtab.c - Executive-resident device table (vms-d0b) + * + * A VMS device is a thing the EXECUTIVE knows about. The driver enters + * a unit in the I/O database at boot and from that moment the device + * exists for every process on the node: $ASSIGN takes a channel to it, + * $GETDVI reads its attributes, $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 the process doing the asking -- which is why a VMS + * terminal name means anything at all. + * + * A device table that lived in a process's own memory would pass every + * single-process test and still be a facade (CLAUDE.md rule 11): the + * decisive check is A-writes / B-reads, and it is what + * tests/qemu/test_kmod_devtab.c does against a real /dev/vms. + * + * The console terminal OPA0: is created here, at module init. No + * process registers it; a process that never asked for it still sees + * it, exactly as on VMS where the terminal driver creates the console + * unit during system initialization. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vms_internal.h" + +/* + * Device class codes. Values mirror src/libvms/include/dcdef.h so the + * executive and the runtime cannot disagree about what a class means. + */ +#define DC__TERM 6 /* DC$_TERM */ + +/* + * Device type codes: 0 is "Unknown". + * + * PROVENANCE (rule 10): the oracle displays an unidentified terminal + * as "Device_Type: Unknown" -- observed by issuing + * SET TERMINAL/DEVICE_TYPE=UNKNOWN on the ~/vax OpenVMS VAX V7.3 lab + * console and reading SHOW TERMINAL back + * (docs/oracle/vax73-terminal-device.md). OVMX's console is a serial + * line whose terminal type is genuinely not identified -- the oracle's + * own login procedure fails the same way on the same kind of console + * ("%SET-W-NOTSET, error modifying OPA0: -SET-I-UNKTERM, unknown + * terminal type") -- so Unknown is what is true here, not a + * placeholder standing in for a value we could not find. + */ +#define VMS_DT_UNKNOWN 0 + +/* ================================================================ + * The table + * ================================================================ */ + +static LIST_HEAD(vms_device_list); +static DEFINE_SPINLOCK(vms_device_list_lock); + +/* + * Console terminal defaults. + * + * PROVENANCE (rule 10, and flagged for operator sign-off per the + * vms-purity-guardrail rule -- these are constants, and constants are + * signed off, not self-certified): + * + * - The name OPA0: is the OpenVMS console terminal, observed on both + * lab nodes (SHOW TERMINAL prints the physical form "_OPA0:"). + * - The characteristic SET below is the set the oracle reports for a + * terminal whose device type is Unknown: Interactive, Echo, + * Type_ahead, TTsync, Lowercase, Wrap, Broadcast, Fulldup, + * Set_speed, Insert editing, Numeric Keypad, VMS Style Input -- + * with everything else clear, notably No Line Editing, which the + * oracle clears when the device type becomes Unknown. + * - HARDCOPY is the one oracle-set bit deliberately NOT copied. On + * the lab the console is a physical LA36 printing terminal and the + * bit survives a device-type change because it describes that + * hardware. OVMX's console is not a printing terminal, so claiming + * Hardcopy would be a statement about our hardware that is false. + * - Width 132 / Page 24 are the pristine console values observed on + * lab node VAX2, which had never had SET TERMINAL issued on it. + * CAVEAT recorded honestly: that console is an LA36 (132-column + * paper), so this is the oracle's console default rather than a + * value derived from OVMX's own serial line, whose real geometry + * the executive cannot interrogate. + */ +#define VMS_CONSOLE_DEVNAM "OPA0:" + +#define VMS_CONSOLE_DEVCHAR (VMS_TTC_INTERACTIVE | \ + VMS_TTC_ECHO | \ + VMS_TTC_TYPEAHEAD | \ + VMS_TTC_TTSYNC | \ + VMS_TTC_LOWERCASE | \ + VMS_TTC_WRAP | \ + VMS_TTC_BROADCAST | \ + VMS_TTC_FULLDUP | \ + VMS_TTC_SET_SPEED | \ + VMS_TTC_INSERT_EDITING | \ + VMS_TTC_NUMERIC_KEYPAD | \ + VMS_TTC_VMS_STYLE_INPUT) + +#define VMS_CONSOLE_WIDTH 132 +#define VMS_CONSOLE_PAGE 24 + +/* + * normalize_devnam - fold a caller-supplied device name into the + * physical form the table is keyed by: upper case, exactly one + * trailing colon, no leading underscore. + * + * Returns 0 on success, or a VMS status on a name that cannot be a + * device name at all. + */ +static uint32_t normalize_devnam(const char *in, char *out, size_t outsz) +{ + size_t i, n = 0; + + if (!in || !out || outsz < 2) + return SS__BADPARAM; + + /* The physical name form drops the leading underscore. */ + if (in[0] == '_') + in++; + + for (i = 0; in[i] != '\0'; i++) { + char c = in[i]; + + if (i >= VMS_DEVNAM_SIZE) /* unterminated / oversized */ + return SS__IVDEVNAM; + if (c == ':') { + if (in[i + 1] != '\0') /* colon must be last */ + return SS__IVDEVNAM; + break; + } + if (!isalnum((unsigned char)c) && c != '$') + return SS__IVDEVNAM; + if (n + 2 >= outsz) + return SS__IVDEVNAM; + out[n++] = (char)toupper((unsigned char)c); + } + + if (n == 0) + return SS__IVDEVNAM; + + out[n++] = ':'; + out[n] = '\0'; + return SS__NORMAL; +} + +/* Caller holds vms_device_list_lock. */ +static struct vms_device *devtab_lookup_locked(const char *devnam) +{ + struct vms_device *dev; + + list_for_each_entry(dev, &vms_device_list, list) { + if (strcmp(dev->devnam, devnam) == 0) + return dev; + } + return NULL; +} + +/* + * vms_devtab_create - enter a unit in the executive's device table. + * + * Called by the executive itself, never from an ioctl: on VMS units + * are created by drivers during system initialization, not by user + * processes asking for them. + */ +static struct vms_device *vms_devtab_create(const char *devnam, + uint32_t devclass, + uint32_t devtype, + uint64_t devchar, + uint32_t width, uint32_t page) +{ + struct vms_device *dev; + + dev = kzalloc(sizeof(*dev), GFP_KERNEL); + if (!dev) + return NULL; + + strscpy(dev->devnam, devnam, sizeof(dev->devnam)); + dev->devclass = devclass; + dev->devtype = devtype; + dev->devchar = devchar; + dev->width = width; + dev->page = page; + spin_lock_init(&dev->lock); + + spin_lock(&vms_device_list_lock); + list_add_tail(&dev->list, &vms_device_list); + spin_unlock(&vms_device_list_lock); + + return dev; +} + +int vms_devtab_init(void) +{ + struct vms_device *console; + + console = vms_devtab_create(VMS_CONSOLE_DEVNAM, DC__TERM, VMS_DT_UNKNOWN, + VMS_CONSOLE_DEVCHAR, + VMS_CONSOLE_WIDTH, VMS_CONSOLE_PAGE); + if (!console) + return -ENOMEM; + + pr_info("vms: device table initialized, console terminal %s created\n", + VMS_CONSOLE_DEVNAM); + return 0; +} + +void vms_devtab_cleanup(void) +{ + struct vms_device *dev, *tmp; + + spin_lock(&vms_device_list_lock); + list_for_each_entry_safe(dev, tmp, &vms_device_list, list) { + list_del(&dev->list); + kfree(dev); + } + spin_unlock(&vms_device_list_lock); +} + +/* ================================================================ + * Channels + * ================================================================ */ + +/* Caller holds proc->chan_lock. */ +static struct vms_channel *chan_find_locked(struct vms_proc *proc, uint32_t chan) +{ + struct vms_channel *ch; + + list_for_each_entry(ch, &proc->channels, list) { + if (ch->chan == chan) + return ch; + } + return NULL; +} + +/* + * Drop one reference to a device. + * + * release_owner_pid, when non-zero, is a process that has just given + * up its LAST channel to this device: if it was the owner, the device + * becomes unowned even though other processes may still hold channels + * to it. Ownership on VMS belongs to a process that has the device; + * a process that has given it back -- or died -- does not still own + * it just because somebody else is using it. + */ +static void device_deref(struct vms_device *dev, pid_t release_owner_pid) +{ + spin_lock(&dev->lock); + if (dev->refcnt > 0) + dev->refcnt--; + if (dev->refcnt == 0 || + (release_owner_pid && dev->owner_linux_pid == release_owner_pid)) { + dev->owner_pid = 0; + dev->owner_linux_pid = 0; + dev->owner_uic = 0; + } + spin_unlock(&dev->lock); +} + +void vms_proc_release_channels(struct vms_proc *proc) +{ + struct vms_channel *ch, *tmp; + LIST_HEAD(doomed); + + spin_lock(&proc->chan_lock); + list_for_each_entry_safe(ch, tmp, &proc->channels, list) + list_move(&ch->list, &doomed); + spin_unlock(&proc->chan_lock); + + /* Every channel this process held is going, so its ownership of + * any device goes with them. */ + list_for_each_entry_safe(ch, tmp, &doomed, list) { + list_del(&ch->list); + device_deref(ch->dev, proc->linux_pid); + kfree(ch); + } +} + +/* ================================================================ + * ioctl handlers + * ================================================================ */ + +long vms_ioctl_assign(struct vms_proc *proc, unsigned long arg) +{ + struct vms_assign_args args; + struct vms_channel *ch; + struct vms_device *dev; + char devnam[VMS_DEVNAM_SIZE]; + uint32_t status; + + memset(&args, 0, sizeof(args)); + if (copy_from_user(&args, (void __user *)arg, sizeof(args))) + return -EFAULT; + args.devnam[VMS_DEVNAM_SIZE - 1] = '\0'; + + status = normalize_devnam(args.devnam, devnam, sizeof(devnam)); + if (status != SS__NORMAL) { + args.chan = 0; + args.status = status; + goto out; + } + + ch = kzalloc(sizeof(*ch), GFP_KERNEL); + if (!ch) + return -ENOMEM; + + spin_lock(&vms_device_list_lock); + dev = devtab_lookup_locked(devnam); + if (!dev) { + spin_unlock(&vms_device_list_lock); + kfree(ch); + args.chan = 0; + args.status = SS__NOSUCHDEV; + goto out; + } + + spin_lock(&dev->lock); + if (dev->owner_pid == 0) { + /* The first channel to an unowned device makes its holder the owner. */ + dev->owner_pid = proc->vms_pid; + dev->owner_linux_pid = proc->linux_pid; + dev->owner_uic = + (((uint32_t)from_kgid(&init_user_ns, current_gid()) & 0xFFFFu) << 16) | + ((uint32_t)from_kuid(&init_user_ns, current_uid()) & 0xFFFFu); + } + dev->refcnt++; + spin_unlock(&dev->lock); + spin_unlock(&vms_device_list_lock); + + ch->dev = dev; + + spin_lock(&proc->chan_lock); + /* + * Channel numbers are opaque to the caller on VMS ("the system + * assigns the channel"), so the allocation policy below -- small + * ascending non-zero integers, never reused within a process -- is + * an OVMX design choice and is not claimed to match VMS's. + */ + ch->chan = ++proc->next_chan; + list_add_tail(&ch->list, &proc->channels); + spin_unlock(&proc->chan_lock); + + args.chan = ch->chan; + args.status = SS__NORMAL; + +out: + if (copy_to_user((void __user *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} + +long vms_ioctl_dassgn(struct vms_proc *proc, unsigned long arg) +{ + struct vms_dassgn_args args; + struct vms_channel *ch; + int last = 0; + + memset(&args, 0, sizeof(args)); + if (copy_from_user(&args, (void __user *)arg, sizeof(args))) + return -EFAULT; + + spin_lock(&proc->chan_lock); + ch = chan_find_locked(proc, args.chan); + if (ch) { + struct vms_channel *other; + + list_del(&ch->list); + /* Does this process still hold another channel to the device? + * If not, it is giving the device up entirely. */ + last = 1; + list_for_each_entry(other, &proc->channels, list) { + if (other->dev == ch->dev) { + last = 0; + break; + } + } + } + spin_unlock(&proc->chan_lock); + + if (!ch) { + args.status = SS__IVCHAN; + } else { + device_deref(ch->dev, last ? proc->linux_pid : 0); + kfree(ch); + args.status = SS__NORMAL; + } + + if (copy_to_user((void __user *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} + +/* Snapshot a device row for userspace. Takes dev->lock. */ +static void devinfo_fill(struct vms_device *dev, struct vms_devinfo *info) +{ + memset(info, 0, sizeof(*info)); + + spin_lock(&dev->lock); + strscpy(info->devnam, dev->devnam, sizeof(info->devnam)); + info->devclass = dev->devclass; + info->devtype = dev->devtype; + info->owner_pid = dev->owner_pid; + info->owner_uic = dev->owner_uic; + info->refcnt = dev->refcnt; + info->errcnt = dev->errcnt; + info->opcnt = dev->opcnt; + info->devchar = dev->devchar; + info->width = dev->width; + info->page = dev->page; + spin_unlock(&dev->lock); +} + +long vms_ioctl_getdvi(struct vms_proc *proc, unsigned long arg) +{ + struct vms_getdvi_args args; + struct vms_device *dev = NULL; + char devnam[VMS_DEVNAM_SIZE]; + uint32_t status; + + memset(&args, 0, sizeof(args)); + if (copy_from_user(&args, (void __user *)arg, sizeof(args))) + return -EFAULT; + + if (args.select == VMS_DVI_SEL_CHAN) { + struct vms_channel *ch; + + spin_lock(&proc->chan_lock); + ch = chan_find_locked(proc, args.chan); + if (ch) + dev = ch->dev; + spin_unlock(&proc->chan_lock); + + if (!dev) { + memset(&args.info, 0, sizeof(args.info)); + args.status = SS__IVCHAN; + goto out; + } + devinfo_fill(dev, &args.info); + args.status = SS__NORMAL; + goto out; + } + + if (args.select != VMS_DVI_SEL_DEVNAM) { + args.status = SS__BADPARAM; + goto out; + } + + args.info.devnam[VMS_DEVNAM_SIZE - 1] = '\0'; + status = normalize_devnam(args.info.devnam, devnam, sizeof(devnam)); + if (status != SS__NORMAL) { + memset(&args.info, 0, sizeof(args.info)); + args.status = status; + goto out; + } + + spin_lock(&vms_device_list_lock); + dev = devtab_lookup_locked(devnam); + if (dev) + devinfo_fill(dev, &args.info); + spin_unlock(&vms_device_list_lock); + + if (!dev) { + memset(&args.info, 0, sizeof(args.info)); + args.status = SS__NOSUCHDEV; + } else { + args.status = SS__NORMAL; + } + +out: + if (copy_to_user((void __user *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} + +long vms_ioctl_devscan(struct vms_proc *proc, unsigned long arg) +{ + struct vms_devscan_args args; + struct vms_device *dev, *found = NULL; + uint32_t i = 0; + + (void)proc; + + memset(&args, 0, sizeof(args)); + if (copy_from_user(&args, (void __user *)arg, sizeof(args))) + return -EFAULT; + + spin_lock(&vms_device_list_lock); + list_for_each_entry(dev, &vms_device_list, list) { + if (i == args.index) { + found = dev; + break; + } + i++; + } + if (found) + devinfo_fill(found, &args.info); + spin_unlock(&vms_device_list_lock); + + if (!found) { + args.status = SS__NOMOREDEV; + } else { + args.index++; + args.status = SS__NORMAL; + } + + if (copy_to_user((void __user *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} + +long vms_ioctl_ttsetmode(struct vms_proc *proc, unsigned long arg) +{ + struct vms_setmode_args args; + struct vms_channel *ch; + struct vms_device *dev = NULL; + + memset(&args, 0, sizeof(args)); + if (copy_from_user(&args, (void __user *)arg, sizeof(args))) + return -EFAULT; + + /* + * Characteristics are set THROUGH A CHANNEL, as VMS sets them + * ($QIO IO$_SETMODE). No channel, no change -- a process cannot + * redefine a terminal it never opened. + */ + spin_lock(&proc->chan_lock); + ch = chan_find_locked(proc, args.chan); + if (ch) + dev = ch->dev; + spin_unlock(&proc->chan_lock); + + if (!dev) { + args.status = SS__IVCHAN; + goto out; + } + + if (dev->devclass != DC__TERM) { + /* IO$_SETMODE terminal function on a non-terminal device. */ + args.status = SS__IVDEVNAM; + goto out; + } + + spin_lock(&dev->lock); + if (args.flags & VMS_TTSET_CHAR) { + dev->devchar |= args.setchar; + dev->devchar &= ~args.clrchar; + } + if (args.flags & VMS_TTSET_WIDTH) + dev->width = args.width; + if (args.flags & VMS_TTSET_PAGE) + dev->page = args.page; + dev->opcnt++; + spin_unlock(&dev->lock); + + args.status = SS__NORMAL; + +out: + if (copy_to_user((void __user *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} diff --git a/src/kernel/vms_internal.h b/src/kernel/vms_internal.h index 646205fe5..e8d5761c9 100644 --- a/src/kernel/vms_internal.h +++ b/src/kernel/vms_internal.h @@ -41,6 +41,24 @@ #define SS__CANCELGRANT 116 /* conversion cancelled */ #define SS__VALNOTVALID 120 /* value block not valid */ +/* + * Device-table statuses. Values are this tree's existing ssdef.h + * (src/libvms/include/ssdef.h) -- they are NOT independently + * re-derived here, so the executive and the runtime cannot drift + * apart. Note that ssdef.h already carries an operator-sign-off flag + * on SS$_NOSUCHDEV / SS$_NOMOREDEV (multi-source disagreement, see + * vms-fb3); this file inherits that caveat rather than papering over + * it. The CHOICE of status per condition is pinned to the oracle + * where observable: SHOW DEVICE of an absent device on the ~/vax + * OpenVMS VAX V7.3 lab reports + * %SYSTEM-W-NOSUCHDEV, no such device available + * (docs/oracle/vax73-terminal-device.md). + */ +#define SS__IVCHAN 602 /* invalid I/O channel */ +#define SS__IVDEVNAM 608 /* invalid device name */ +#define SS__NOMOREDEV 2648 /* device scan exhausted */ +#define SS__NOSUCHDEV 2680 /* no such device available */ + /* * Default privilege set for non-CAP_SYS_ADMIN processes. * Allows basic operational use (mailboxes, networking) without @@ -153,9 +171,62 @@ struct vms_proc { int lock_count; spinlock_t lock_list_lock; + /* + * I/O channels (device table, vms-d0b). A channel is this + * process's handle on a device that the EXECUTIVE owns -- the + * device itself is not per-process, only the channel to it is. + * Released when the process's executive state is torn down, which + * is what drops the device's reference count and its ownership. + */ + struct list_head channels; /* struct vms_channel */ + uint32_t next_chan; /* channel number allocator */ + spinlock_t chan_lock; + struct rcu_head rcu; }; +/* ================================================================ + * Device table (executive-resident I/O database) + * + * One entry per device on the node, created by the executive and + * visible to every process. See vms_devtab.c. + * ================================================================ */ + +struct vms_device { + struct list_head list; /* in vms_device_list */ + char devnam[VMS_DEVNAM_SIZE]; + uint32_t devclass; /* DC$_ device class */ + uint32_t devtype; /* device type code; 0 = Unknown */ + + /* + * Ownership. owner_pid is the VMS pid of the process that first + * took a channel to the device and is held until the LAST channel + * is given back -- SHOW DEVICE/FULL's "Owner process ID" together + * with "Reference count". + */ + uint32_t owner_pid; + pid_t owner_linux_pid; + uint32_t owner_uic; + uint32_t refcnt; + + uint32_t errcnt; + uint64_t opcnt; + + /* Terminal state (devclass == DC$_TERM) */ + uint64_t devchar; /* VMS_TTC_* */ + uint32_t width; + uint32_t page; + + spinlock_t lock; +}; + +/* A process's handle on a device. */ +struct vms_channel { + struct list_head list; /* in vms_proc->channels */ + uint32_t chan; + struct vms_device *dev; +}; + /* ================================================================ * Global module state * ================================================================ */ @@ -219,11 +290,23 @@ long vms_ioctl_deq(struct vms_proc *proc, unsigned long arg); long vms_ioctl_convert(struct vms_proc *proc, unsigned long arg); long vms_ioctl_getlki(struct vms_proc *proc, unsigned long arg); +/* Device table (executive-resident I/O database) */ +long vms_ioctl_assign(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_dassgn(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_getdvi(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_devscan(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_ttsetmode(struct vms_proc *proc, unsigned long arg); + /* Subsystem init/cleanup */ int vms_lock_init(void); void vms_lock_cleanup(void); void vms_eflag_init(void); void vms_eflag_cleanup(void); +int vms_devtab_init(void); +void vms_devtab_cleanup(void); + +/* Give back every channel a process holds (process teardown). */ +void vms_proc_release_channels(struct vms_proc *proc); /* Lock manager helpers */ void vms_proc_release_locks(struct vms_proc *proc); diff --git a/src/kernel/vms_ioctl.h b/src/kernel/vms_ioctl.h index 02abb2b11..78df27186 100644 --- a/src/kernel/vms_ioctl.h +++ b/src/kernel/vms_ioctl.h @@ -218,4 +218,190 @@ struct vms_register_args { #define VMS_IOCTL_REGISTER _IOWR(VMS_IOC_MAGIC, 0x40, struct vms_register_args) +/* ================================================================ + * Device table (executive-resident I/O database) + * + * On OpenVMS a device is not something a process owns a private idea + * of: the driver enters a Unit Control Block in the executive's 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 + * its attributes, $DEVICE_SCAN enumerates it, and SHOW DEVICE / + * SHOW TERMINAL are readers of that one table. The owner, the + * reference count and the terminal characteristics are properties of + * the device, not of whoever happens to be asking. + * + * These ioctls put the same property behind /dev/vms (vms-d0b). The + * console terminal OPA0: is created by the executive at module init, + * exactly as the terminal driver creates it at VMS boot -- no process + * registers it, and no process can be the only one that sees it. + * ================================================================ */ + +/* + * VMS device names: at most 15 significant characters plus the + * terminating NUL (OpenVMS I/O User's Reference Manual; the physical + * name form is ddcu:, e.g. OPA0:). + */ +#define VMS_DEVNAM_SIZE 16 + +/* + * Terminal characteristics. + * + * PROVENANCE (CLAUDE.md rules 8 and 10): the NAMES below and the fact + * that each is a two-state characteristic are pinned to the oracle -- + * SHOW TERMINAL on the ~/vax OpenVMS VAX V7.3 lab console (nodes VAX1 + * and VAX2, 30-JUL-2026), captured verbatim in + * docs/oracle/vax73-terminal-device.md. Every name here appears in + * that output; no name was invented, and no characteristic VMS does + * not display was added. + * + * The BIT POSITIONS are an OVMX design choice and are NOT VMS's + * $TTDEF layout. The public OpenVMS documentation available to this + * work does not publish the byte-level TT$M_ layout, so rather than + * guess at it OVMX defines its own vector and labels it as its own + * (rule 8: define our representation, never present it as + * VMS-authentic). Nothing outside the executive and its client may + * assume these values match VMS. + */ +#define VMS_TTC_INTERACTIVE (1ULL << 0) +#define VMS_TTC_ECHO (1ULL << 1) +#define VMS_TTC_TYPEAHEAD (1ULL << 2) +#define VMS_TTC_ESCAPE (1ULL << 3) +#define VMS_TTC_HOSTSYNC (1ULL << 4) +#define VMS_TTC_TTSYNC (1ULL << 5) +#define VMS_TTC_LOWERCASE (1ULL << 6) +#define VMS_TTC_TAB (1ULL << 7) +#define VMS_TTC_WRAP (1ULL << 8) +#define VMS_TTC_HARDCOPY (1ULL << 9) +#define VMS_TTC_REMOTE (1ULL << 10) +#define VMS_TTC_EIGHTBIT (1ULL << 11) +#define VMS_TTC_BROADCAST (1ULL << 12) +#define VMS_TTC_READSYNC (1ULL << 13) +#define VMS_TTC_FORM (1ULL << 14) +#define VMS_TTC_FULLDUP (1ULL << 15) +#define VMS_TTC_MODEM (1ULL << 16) +#define VMS_TTC_LOCAL_ECHO (1ULL << 17) +#define VMS_TTC_AUTOBAUD (1ULL << 18) +#define VMS_TTC_HANGUP (1ULL << 19) +#define VMS_TTC_BRDCSTMBX (1ULL << 20) +#define VMS_TTC_DMA (1ULL << 21) +#define VMS_TTC_ALTYPEAHD (1ULL << 22) +#define VMS_TTC_SET_SPEED (1ULL << 23) +#define VMS_TTC_COMMSYNC (1ULL << 24) +#define VMS_TTC_LINE_EDITING (1ULL << 25) +#define VMS_TTC_INSERT_EDITING (1ULL << 26) +#define VMS_TTC_FALLBACK (1ULL << 27) +#define VMS_TTC_DIALUP (1ULL << 28) +#define VMS_TTC_SECURE_SERVER (1ULL << 29) +#define VMS_TTC_DISCONNECT (1ULL << 30) +#define VMS_TTC_PASTHRU (1ULL << 31) +#define VMS_TTC_SYSPASSWORD (1ULL << 32) +#define VMS_TTC_SIXEL (1ULL << 33) +#define VMS_TTC_SOFT_CHARACTERS (1ULL << 34) +#define VMS_TTC_PRINTER_PORT (1ULL << 35) +#define VMS_TTC_NUMERIC_KEYPAD (1ULL << 36) +#define VMS_TTC_ANSI_CRT (1ULL << 37) +#define VMS_TTC_REGIS (1ULL << 38) +#define VMS_TTC_BLOCK_MODE (1ULL << 39) +#define VMS_TTC_ADVANCED_VIDEO (1ULL << 40) +#define VMS_TTC_EDIT_MODE (1ULL << 41) +#define VMS_TTC_DEC_CRT (1ULL << 42) +#define VMS_TTC_DEC_CRT2 (1ULL << 43) +#define VMS_TTC_DEC_CRT3 (1ULL << 44) +#define VMS_TTC_DEC_CRT4 (1ULL << 45) +#define VMS_TTC_DEC_CRT5 (1ULL << 46) +#define VMS_TTC_ANSI_COLOR (1ULL << 47) +#define VMS_TTC_VMS_STYLE_INPUT (1ULL << 48) + +/* + * One row of the executive device table, as handed to userspace. + * + * owner_pid is 0 when the device is unowned. opcnt/errcnt are the + * "Operations completed" and "Error count" SHOW DEVICE/FULL reports. + * + * NOTE what is deliberately ABSENT (rule 10 -- hide what we cannot + * answer faithfully rather than reporting a plausible value): the + * oracle's SHOW TERMINAL also prints Input/Output speed, Parity, and + * LFfill/CRfill. OVMX's console is a QEMU serial line with no such + * physical parameters to report, so the executive does not carry a + * value for them and no reader can print one. + */ +struct vms_devinfo { + char devnam[VMS_DEVNAM_SIZE]; /* physical name, e.g. "OPA0:" */ + uint32_t devclass; /* DC$_ device class */ + uint32_t devtype; /* device type code; 0 = Unknown */ + uint32_t owner_pid; /* VMS pid of the owner, 0 = unowned */ + uint32_t owner_uic; /* (group << 16) | member */ + uint32_t refcnt; /* channels currently assigned */ + uint32_t errcnt; /* Error count */ + uint64_t opcnt; /* Operations completed */ + uint64_t devchar; /* VMS_TTC_* (terminals only) */ + uint32_t width; /* terminal width */ + uint32_t page; /* terminal page length */ +}; + +/* $ASSIGN: take a channel to a device by name. */ +struct vms_assign_args { + char devnam[VMS_DEVNAM_SIZE]; /* in: device name (with or without ':') */ + uint32_t chan; /* out: channel number */ + uint32_t status; /* return: SS$_ status */ +}; + +/* $DASSGN: give a channel back. */ +struct vms_dassgn_args { + uint32_t chan; + uint32_t status; +}; + +/* Selector for VMS_IOCTL_GETDVI: how the device is named. */ +#define VMS_DVI_SEL_DEVNAM 0 /* by info.devnam */ +#define VMS_DVI_SEL_CHAN 1 /* by an assigned channel */ + +struct vms_getdvi_args { + uint32_t select; /* VMS_DVI_SEL_* */ + uint32_t chan; /* in: channel, for VMS_DVI_SEL_CHAN */ + uint32_t status; /* return: SS$_ status */ + uint32_t pad; + struct vms_devinfo info; /* in: name for SEL_DEVNAM; out: the row */ +}; + +/* + * Cursor-driven enumeration of the device table (the reader behind + * SHOW DEVICE). Set index to 0 for the first row; each call returns + * one row and advances index. SS$_NOMOREDEV terminates the scan, + * which is what $DEVICE_SCAN returns when the search is exhausted. + */ +struct vms_devscan_args { + uint32_t index; /* in: cursor; out: cursor for next call */ + uint32_t status; /* return: SS$_ status */ + struct vms_devinfo info; /* out: the row at the incoming cursor */ +}; + +/* + * Modify terminal characteristics THROUGH AN ASSIGNED CHANNEL, the + * way VMS does it: SET TERMINAL is $QIO IO$_SETMODE on a channel, not + * an operation that names a device out of nowhere. A caller with no + * channel to the device gets SS$_IVCHAN, which is why a process + * cannot quietly redefine a terminal it never opened. + */ +#define VMS_TTSET_CHAR 0x1 /* apply setchar/clrchar */ +#define VMS_TTSET_WIDTH 0x2 /* apply width */ +#define VMS_TTSET_PAGE 0x4 /* apply page */ + +struct vms_setmode_args { + uint32_t chan; /* channel assigned to the terminal */ + uint32_t flags; /* VMS_TTSET_* : which fields are being set */ + uint64_t setchar; /* VMS_TTC_* bits to set */ + uint64_t clrchar; /* VMS_TTC_* bits to clear */ + uint32_t width; + uint32_t page; + uint32_t status; /* return: SS$_ status */ + uint32_t pad; +}; + +#define VMS_IOCTL_ASSIGN _IOWR(VMS_IOC_MAGIC, 0x50, struct vms_assign_args) +#define VMS_IOCTL_DASSGN _IOWR(VMS_IOC_MAGIC, 0x51, struct vms_dassgn_args) +#define VMS_IOCTL_GETDVI _IOWR(VMS_IOC_MAGIC, 0x52, struct vms_getdvi_args) +#define VMS_IOCTL_DEVSCAN _IOWR(VMS_IOC_MAGIC, 0x53, struct vms_devscan_args) +#define VMS_IOCTL_TTSETMODE _IOWR(VMS_IOC_MAGIC, 0x54, struct vms_setmode_args) + #endif /* _VMS_IOCTL_H */ diff --git a/src/kernel/vms_module.c b/src/kernel/vms_module.c index 3a73ef925..8fa6ecbde 100644 --- a/src/kernel/vms_module.c +++ b/src/kernel/vms_module.c @@ -110,6 +110,11 @@ struct vms_proc *vms_proc_register(pid_t pid, uint32_t vms_pid, uint64_t init_pr proc->lock_count = 0; spin_lock_init(&proc->lock_list_lock); + /* Initialize the I/O channel list (device table, vms-d0b) */ + INIT_LIST_HEAD(&proc->channels); + proc->next_chan = 0; + spin_lock_init(&proc->chan_lock); + /* Atomically check-and-insert under spinlock to avoid TOCTOU race */ spin_lock(&vms_proc_hash_lock); hash_for_each_possible_rcu(vms_proc_hash, existing, hash_node, pid) { @@ -154,6 +159,14 @@ void vms_proc_free(struct vms_proc *proc) /* Release common event flag associations */ vms_proc_release_common_ef(proc); + /* + * Give back every I/O channel. The devices themselves belong to + * the executive and outlive the process; what dies here is this + * process's claim on them, which is what releases device + * ownership when the last channel goes. + */ + vms_proc_release_channels(proc); + /* RCU-deferred free — proc may still be accessed by RCU readers */ kfree_rcu(proc, rcu); } @@ -246,6 +259,18 @@ static long vms_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg case VMS_IOCTL_GETLKI: return vms_ioctl_getlki(proc, arg); + /* Device table (executive-resident I/O database) */ + case VMS_IOCTL_ASSIGN: + return vms_ioctl_assign(proc, arg); + case VMS_IOCTL_DASSGN: + return vms_ioctl_dassgn(proc, arg); + case VMS_IOCTL_GETDVI: + return vms_ioctl_getdvi(proc, arg); + case VMS_IOCTL_DEVSCAN: + return vms_ioctl_devscan(proc, arg); + case VMS_IOCTL_TTSETMODE: + return vms_ioctl_ttsetmode(proc, arg); + default: return -ENOTTY; } @@ -308,10 +333,28 @@ static int __init vms_init(void) } vms_eflag_init(); + /* + * Bring up the device table before /dev/vms exists, so that the + * console terminal is in the executive's I/O database before any + * process can possibly ask about it -- a device is never something + * a process introduces. + */ + ret = vms_devtab_init(); + if (ret) { + pr_err("vms: failed to initialize device table: %d\n", ret); + vms_lock_cleanup(); + vms_eflag_cleanup(); + kmem_cache_destroy(vms_proc_cache); + return ret; + } + /* Register /dev/vms */ ret = misc_register(&vms_misc); if (ret) { pr_err("vms: failed to register /dev/vms: %d\n", ret); + vms_devtab_cleanup(); + vms_lock_cleanup(); + vms_eflag_cleanup(); kmem_cache_destroy(vms_proc_cache); return ret; } @@ -341,6 +384,7 @@ static void __exit vms_exit(void) /* Cleanup subsystems */ vms_lock_cleanup(); vms_eflag_cleanup(); + vms_devtab_cleanup(); kmem_cache_destroy(vms_proc_cache); diff --git a/src/libvmssys/vms_kif.c b/src/libvmssys/vms_kif.c index b6db08e8b..881095d08 100644 --- a/src/libvmssys/vms_kif.c +++ b/src/libvmssys/vms_kif.c @@ -376,3 +376,125 @@ uint32_t vms_kif_getlki(uint32_t lkid, uint32_t *granted_mode, return args.status; } + +/* ================================================================ + * Device table (executive-resident I/O database) + * + * These are readers of, and a channel-scoped writer to, shared state + * the executive owns. What they report about a device is what every + * other process on the node sees -- not a per-process idea of what a + * device looks like. + * ================================================================ */ + +uint32_t vms_kif_assign(const char *devnam, uint32_t *chan) +{ + struct vms_assign_args args; + + if (!devnam || !chan) + return 0x00000014; /* SS$_BADPARAM */ + + vms_memset(&args, 0, sizeof(args)); + vms_strncpy(args.devnam, devnam, VMS_DEVNAM_SIZE - 1); + args.devnam[VMS_DEVNAM_SIZE - 1] = '\0'; + + if (vms_sys_ioctl(vms_dev_fd, VMS_IOCTL_ASSIGN, (unsigned long)&args) < 0) + return 0x00000014; + + /* VMS writes the channel only on success (odd status); a failed + * $ASSIGN must not disturb the caller's channel variable. */ + if (args.status & 1) + *chan = args.chan; + + return args.status; +} + +uint32_t vms_kif_dassgn(uint32_t chan) +{ + struct vms_dassgn_args args; + + vms_memset(&args, 0, sizeof(args)); + args.chan = chan; + + if (vms_sys_ioctl(vms_dev_fd, VMS_IOCTL_DASSGN, (unsigned long)&args) < 0) + return 0x00000014; + + return args.status; +} + +uint32_t vms_kif_getdvi_devnam(const char *devnam, struct vms_devinfo *info) +{ + struct vms_getdvi_args args; + + if (!devnam) + return 0x00000014; /* SS$_BADPARAM */ + + vms_memset(&args, 0, sizeof(args)); + args.select = VMS_DVI_SEL_DEVNAM; + vms_strncpy(args.info.devnam, devnam, VMS_DEVNAM_SIZE - 1); + args.info.devnam[VMS_DEVNAM_SIZE - 1] = '\0'; + + if (vms_sys_ioctl(vms_dev_fd, VMS_IOCTL_GETDVI, (unsigned long)&args) < 0) + return 0x00000014; + + if (info) + vms_memcpy(info, &args.info, sizeof(*info)); + + return args.status; +} + +uint32_t vms_kif_getdvi_chan(uint32_t chan, struct vms_devinfo *info) +{ + struct vms_getdvi_args args; + + vms_memset(&args, 0, sizeof(args)); + args.select = VMS_DVI_SEL_CHAN; + args.chan = chan; + + if (vms_sys_ioctl(vms_dev_fd, VMS_IOCTL_GETDVI, (unsigned long)&args) < 0) + return 0x00000014; + + if (info) + vms_memcpy(info, &args.info, sizeof(*info)); + + return args.status; +} + +uint32_t vms_kif_devscan(uint32_t *index, struct vms_devinfo *info) +{ + struct vms_devscan_args args; + + if (!index) + return 0x00000014; /* SS$_BADPARAM */ + + vms_memset(&args, 0, sizeof(args)); + args.index = *index; + + if (vms_sys_ioctl(vms_dev_fd, VMS_IOCTL_DEVSCAN, (unsigned long)&args) < 0) + return 0x00000014; + + *index = args.index; + if (info) + vms_memcpy(info, &args.info, sizeof(*info)); + + return args.status; +} + +uint32_t vms_kif_ttsetmode(uint32_t chan, uint32_t flags, + uint64_t setchar, uint64_t clrchar, + uint32_t width, uint32_t page) +{ + struct vms_setmode_args args; + + vms_memset(&args, 0, sizeof(args)); + args.chan = chan; + args.flags = flags; + args.setchar = setchar; + args.clrchar = clrchar; + args.width = width; + args.page = page; + + if (vms_sys_ioctl(vms_dev_fd, VMS_IOCTL_TTSETMODE, (unsigned long)&args) < 0) + return 0x00000014; + + return args.status; +} diff --git a/src/libvmssys/vms_kif.h b/src/libvmssys/vms_kif.h index ae627fba1..826abc4b1 100644 --- a/src/libvmssys/vms_kif.h +++ b/src/libvmssys/vms_kif.h @@ -110,4 +110,41 @@ uint32_t vms_kif_getlki(uint32_t lkid, uint32_t *granted_mode, uint32_t *requested_mode, char *resnam, uint8_t *valblk); +/* ================================================================ + * Device table (executive-resident I/O database) + * + * The executive owns the devices; a process owns only its channels to + * them. A device attribute read here is the attribute every process + * on the node sees, and a characteristic set here is seen by every + * process on the node -- which is the whole difference between a VMS + * device and a private notion of one. + * ================================================================ */ + +/* $ASSIGN a channel to a device by name. SS$_NOSUCHDEV if the + * executive has no such device; SS$_IVDEVNAM if the name is not a + * device name at all. */ +uint32_t vms_kif_assign(const char *devnam, uint32_t *chan); + +/* $DASSGN the channel. SS$_IVCHAN if it is not one of ours. */ +uint32_t vms_kif_dassgn(uint32_t chan); + +/* Read a device row by name. SS$_NOSUCHDEV if there is no such device. */ +uint32_t vms_kif_getdvi_devnam(const char *devnam, struct vms_devinfo *info); + +/* Read the device row behind an assigned channel. SS$_IVCHAN if the + * channel is not ours. */ +uint32_t vms_kif_getdvi_chan(uint32_t chan, struct vms_devinfo *info); + +/* Enumerate the device table. Pass *index = 0 for the first row; each + * call fills info and advances *index. Returns SS$_NOMOREDEV when the + * scan is exhausted. */ +uint32_t vms_kif_devscan(uint32_t *index, struct vms_devinfo *info); + +/* Set terminal characteristics through an assigned channel (the + * $QIO IO$_SETMODE path). flags is a mask of VMS_TTSET_*; SS$_IVCHAN + * if the caller holds no such channel. */ +uint32_t vms_kif_ttsetmode(uint32_t chan, uint32_t flags, + uint64_t setchar, uint64_t clrchar, + uint32_t width, uint32_t page); + #endif /* _VMS_KIF_H */ diff --git a/tests/qemu/Dockerfile b/tests/qemu/Dockerfile index df882a9ae..04677bf69 100644 --- a/tests/qemu/Dockerfile +++ b/tests/qemu/Dockerfile @@ -67,11 +67,27 @@ RUN KVER=$(cat /tmp/kver) && \ # Copy test source and build statically-linked test programs COPY tests/qemu/test_*.c /src/tests/qemu/ RUN for f in /src/tests/qemu/test_*.c; do \ + case "$f" in */test_kmod_devtab.c) continue;; esac; \ out="${f%.c}"; \ gcc -static -O2 -Wall -o "$out" "$f" -I/src/kernel && \ echo "Built: $(basename $out)"; \ done +# The device-table test (vms-d0b) drives the real userspace kernel-interface +# client rather than a hand-rolled ioctl copy, so the client libvms will call is +# the client under test. That needs libvmssys's freestanding syscall trampoline +# and string helpers linked in -- it does NOT pull in libvms, which still cannot +# be built into this initramfs (tracked separately). +COPY src/libvmssys/ /src/libvmssys/ +RUN ARCH=$(uname -m) && \ + gcc -static -O2 -Wall -o /src/tests/qemu/test_kmod_devtab \ + /src/tests/qemu/test_kmod_devtab.c \ + /src/libvmssys/vms_kif.c \ + /src/libvmssys/vms_string.c \ + /src/libvmssys/arch/${ARCH}/syscall.S \ + -I/src/kernel -I/src/libvmssys && \ + echo "Built: test_kmod_devtab (linked against src/libvmssys/vms_kif.c)" + # Build mkimage_vmsfs (host tool) and create test image COPY tests/qemu/mkimage_vmsfs.c /src/tests/qemu/ RUN gcc -O2 -Wall -o /src/tests/qemu/mkimage_vmsfs \ diff --git a/tests/qemu/test_kmod_devtab.c b/tests/qemu/test_kmod_devtab.c new file mode 100644 index 000000000..79047b61c --- /dev/null +++ b/tests/qemu/test_kmod_devtab.c @@ -0,0 +1,305 @@ +/* + * test_kmod_devtab.c - Executive device table: A-writes / B-reads (vms-d0b) + * + * A terminal is only a VMS device because the whole system agrees it + * exists. A device table a process keeps in its own address space + * passes every single-process test perfectly and is still a facade -- + * it reports success while sharing nothing (CLAUDE.md rule 11). So + * this test is built around the check that can tell the difference: + * + * PROCESS A ASSIGNS THE DEVICE AND CHANGES IT. + * PROCESS B, WHICH DID NOTHING, SEES THE CHANGE. + * + * and around the property that makes the device the executive's and + * not the process's: + * + * THE DEVICE EXISTS BEFORE ANY PROCESS ASKS FOR IT, AND IT SURVIVES + * THE DEATH OF THE PROCESS THAT OWNED IT. + * + * The test drives the real userspace client (src/libvmssys/vms_kif.c) + * against a real /dev/vms, not a hand-rolled ioctl copy, so the client + * that libvms will call is the client under test. + * + * Modes: + * (no args) process B (parent) + * --owner process A: assigns OPA0:, changes it, reports + */ + +#include +#include +#include +#include +#include +#include + +#include "vms_kif.h" + +#define SS_NORMAL 1 +#define SS_BADPARAM 20 +#define SS_IVCHAN 602 +#define SS_IVDEVNAM 608 +#define SS_NOMOREDEV 2648 +#define SS_NOSUCHDEV 2680 + +#define DC_TERM 6 + +#define CONSOLE "OPA0:" +#define ABSENT_DEV "ZZA0:" + +/* What process A changes the console to, so B can look for it. */ +#define A_WIDTH 64 +#define A_PAGE 40 + +static int pass = 0, fail = 0; + +#define CHECK(cond, msg) do { \ + if (cond) { printf(" PASS: %s\n", msg); pass++; } \ + else { printf(" FAIL: %s\n", msg); fail++; } \ +} while (0) + +/* What process A reports back over a pipe. */ +struct owner_report { + uint32_t assign_status; + uint32_t setmode_status; + uint32_t chan; + uint32_t owner_pid; /* owner the executive shows A after assign */ + uint32_t refcnt; +}; + +static int open_and_register(void) +{ + if (vms_kif_open() < 0) { + printf(" FAIL: cannot open /dev/vms (executive absent)\n"); + return -1; + } + if (vms_kif_register((uint32_t)getpid(), 0) != SS_NORMAL) { + printf(" FAIL: VMS_IOCTL_REGISTER rejected\n"); + return -1; + } + return 0; +} + +/* + * process_a - assigns a channel to the console, becomes its owner, and + * changes characteristics that it never tells anyone about except + * through the executive. + */ +static int process_a(int wfd) +{ + struct owner_report rep; + struct vms_devinfo info; + + memset(&rep, 0, sizeof(rep)); + + if (vms_kif_open() < 0) { + (void)!write(wfd, &rep, sizeof(rep)); + return 1; + } + if (vms_kif_register((uint32_t)getpid(), 0) != SS_NORMAL) { + (void)!write(wfd, &rep, sizeof(rep)); + return 1; + } + + rep.assign_status = vms_kif_assign(CONSOLE, &rep.chan); + if (rep.assign_status == SS_NORMAL) { + rep.setmode_status = vms_kif_ttsetmode( + rep.chan, + VMS_TTSET_CHAR | VMS_TTSET_WIDTH | VMS_TTSET_PAGE, + VMS_TTC_PASTHRU, /* set */ + VMS_TTC_ECHO, /* clear */ + A_WIDTH, A_PAGE); + + memset(&info, 0, sizeof(info)); + if (vms_kif_getdvi_chan(rep.chan, &info) == SS_NORMAL) { + rep.owner_pid = info.owner_pid; + rep.refcnt = info.refcnt; + } + } + + if (write(wfd, &rep, sizeof(rep)) != (ssize_t)sizeof(rep)) + return 1; + close(wfd); + + /* Stay alive and owning the device until the parent kills us. */ + for (;;) + pause(); + + return 0; +} + +int main(int argc, char **argv) +{ + int pipefd[2]; + pid_t child; + struct owner_report rep; + struct vms_devinfo info; + uint32_t status, chan = 0, bogus_chan, index, scanned, saw_console; + char wfd_arg[16]; + + if (argc >= 3 && strcmp(argv[1], "--owner") == 0) + return process_a(atoi(argv[2])); + + printf("=== test_kmod_devtab: executive device table ===\n"); + + if (open_and_register() < 0) { + printf("=== test_kmod_devtab: 0 passed, 1 failed ===\n"); + return 1; + } + + /* -------------------------------------------------------------- + * 1. The device exists before anyone asks for it. Nothing in this + * process created OPA0:; the executive did, at module init, + * the way a VMS driver creates the console unit at boot. + * -------------------------------------------------------------- */ + memset(&info, 0, sizeof(info)); + status = vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(status == SS_NORMAL, "console OPA0: exists without any process creating it"); + CHECK(strcmp(info.devnam, CONSOLE) == 0, "device reports its VMS physical name"); + CHECK(info.devclass == DC_TERM, "console is a terminal-class device (DC$_TERM)"); + CHECK(info.owner_pid == 0, "console starts unowned"); + + /* Negative control: a lookup that always succeeded would look + * identical to a lookup that works. */ + memset(&info, 0, sizeof(info)); + status = vms_kif_getdvi_devnam(ABSENT_DEV, &info); + CHECK(status == SS_NOSUCHDEV, "absent device reports SS$_NOSUCHDEV"); + + status = vms_kif_getdvi_devnam("not a device", &info); + CHECK(status == SS_IVDEVNAM, "malformed device name reports SS$_IVDEVNAM"); + + /* A channel we never assigned is not ours. */ + status = vms_kif_getdvi_chan(4242, &info); + CHECK(status == SS_IVCHAN, "unassigned channel reports SS$_IVCHAN"); + status = vms_kif_ttsetmode(4242, VMS_TTSET_WIDTH, 0, 0, 100, 0); + CHECK(status == SS_IVCHAN, + "cannot set characteristics without a channel (SS$_IVCHAN)"); + + /* -------------------------------------------------------------- + * 2. The scan enumerates the table, and terminates. + * -------------------------------------------------------------- */ + scanned = 0; + saw_console = 0; + index = 0; + for (;;) { + memset(&info, 0, sizeof(info)); + status = vms_kif_devscan(&index, &info); + if (status != SS_NORMAL) + break; + scanned++; + if (strcmp(info.devnam, CONSOLE) == 0) + saw_console = 1; + if (scanned > 64) + break; + } + CHECK(status == SS_NOMOREDEV, "device scan terminates with SS$_NOMOREDEV"); + CHECK(saw_console, "device scan lists the console terminal"); + + /* -------------------------------------------------------------- + * 3. A-writes / B-reads. A different process assigns the console, + * takes ownership and changes its characteristics. This process + * did none of that and must see all of it. + * -------------------------------------------------------------- */ + if (pipe(pipefd) < 0) { + printf(" FAIL: pipe()\n"); + return 1; + } + + child = fork(); + if (child < 0) { + printf(" FAIL: fork()\n"); + return 1; + } + + if (child == 0) { + close(pipefd[0]); + /* Take our own channel rather than the inherited descriptor. */ + vms_kif_close(); + snprintf(wfd_arg, sizeof(wfd_arg), "%d", pipefd[1]); + execl(argv[0], argv[0], "--owner", wfd_arg, (char *)NULL); + _exit(73); + } + + close(pipefd[1]); + memset(&rep, 0, sizeof(rep)); + if (read(pipefd[0], &rep, sizeof(rep)) != (ssize_t)sizeof(rep)) { + printf(" FAIL: owner process never reported\n"); + kill(child, SIGKILL); + waitpid(child, NULL, 0); + printf("=== test_kmod_devtab: %d passed, %d failed ===\n", pass, fail + 1); + return 1; + } + + CHECK(rep.assign_status == SS_NORMAL, "another process assigns a channel to OPA0:"); + CHECK(rep.setmode_status == SS_NORMAL, "owner sets terminal characteristics"); + CHECK(rep.owner_pid == (uint32_t)child, "executive records the assigning process as owner"); + CHECK(rep.refcnt == 1, "executive counts one channel to the device"); + + memset(&info, 0, sizeof(info)); + status = vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(status == SS_NORMAL, "this process can still read the device"); + CHECK(info.owner_pid == (uint32_t)child, + "B sees the owner A took (A writes, B reads)"); + CHECK(info.refcnt == 1, "B sees A's reference count"); + CHECK(info.width == A_WIDTH && info.page == A_PAGE, + "B sees the width and page A set"); + CHECK((info.devchar & VMS_TTC_PASTHRU) != 0, + "B sees the characteristic A set (Pasthru)"); + CHECK((info.devchar & VMS_TTC_ECHO) == 0, + "B sees the characteristic A cleared (No Echo)"); + + /* -------------------------------------------------------------- + * 4. B assigns the same device: the device is shared, and B does + * NOT displace A as its owner. + * -------------------------------------------------------------- */ + status = vms_kif_assign(CONSOLE, &chan); + CHECK(status == SS_NORMAL && chan != 0, "B assigns its own channel to the same device"); + + memset(&info, 0, sizeof(info)); + status = vms_kif_getdvi_chan(chan, &info); + CHECK(status == SS_NORMAL && info.refcnt == 2, + "device reference count counts both processes"); + CHECK(info.owner_pid == (uint32_t)child, + "a second assigner does not steal ownership"); + + /* A failed $ASSIGN must not disturb the channel we already hold. */ + bogus_chan = chan; + status = vms_kif_assign(ABSENT_DEV, &bogus_chan); + CHECK(status == SS_NOSUCHDEV, "assigning an absent device fails with SS$_NOSUCHDEV"); + CHECK(bogus_chan == chan, "failed $ASSIGN leaves the caller's channel untouched"); + + /* -------------------------------------------------------------- + * 5. The device outlives its owner. When A dies the executive + * takes the ownership back -- the device is the executive's, + * not A's. + * -------------------------------------------------------------- */ + kill(child, SIGKILL); + waitpid(child, NULL, 0); + + memset(&info, 0, sizeof(info)); + status = vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(status == SS_NORMAL, "device still exists after its owner dies"); + CHECK(info.owner_pid == 0, "dead process no longer owns the device"); + CHECK(info.refcnt == 1, "dead process's channel was released"); + CHECK(info.width == A_WIDTH && info.page == A_PAGE, + "characteristics set by the dead process persist in the executive"); + + /* -------------------------------------------------------------- + * 6. Giving a channel back releases the reference and, with it, + * ownership of the device. + * -------------------------------------------------------------- */ + status = vms_kif_dassgn(chan); + CHECK(status == SS_NORMAL, "channel deassigned"); + status = vms_kif_dassgn(chan); + CHECK(status == SS_IVCHAN, "deassigning a released channel reports SS$_IVCHAN"); + + memset(&info, 0, sizeof(info)); + status = vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(status == SS_NORMAL && info.refcnt == 0, + "reference count returns to zero"); + CHECK(info.owner_pid == 0, "device is unowned once the last channel is gone"); + + vms_kif_close(); + + printf("=== test_kmod_devtab: %d passed, %d failed ===\n", pass, fail); + return fail > 0 ? 1 : 0; +} From 8ef35442a8b6807e9806583d4f613eaaf22e17e5 Mon Sep 17 00:00:00 2001 From: Chris Baron Date: Thu, 30 Jul 2026 09:15:39 -0400 Subject: [PATCH 2/8] vms-d0b: mark the terminal-identity facades as stopgaps, loudly 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) --- src/ovmx_init/ovmx_init.c | 19 ++++++++++++++++++- src/vmsdcl/dcl_cmd_show.c | 30 ++++++++++++++++++++++++++++-- src/vmsdcl/dcl_main.c | 20 ++++++++++++++++++++ src/vmsssh/vmssshd.c | 12 ++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/ovmx_init/ovmx_init.c b/src/ovmx_init/ovmx_init.c index e7e1b4957..9bc7b5b07 100644 --- a/src/ovmx_init/ovmx_init.c +++ b/src/ovmx_init/ovmx_init.c @@ -818,7 +818,24 @@ int main(void) pid_t child = fork(); if (child == 0) { - /* Console terminal device is _OPA0: */ + /* + * STOPGAP -- FACADE, NOT VMS (vms-d0b). The console + * terminal device is _OPA0:, and as of vms-d0b that is a + * real device in the executive's device table + * (src/kernel/vms_devtab.c), created by the executive at + * module init and visible to every process on the node. + * Handing the name down in an environment variable is the + * rejected VMS_PRCNAM shape (CLAUDE.md rule 10, worked + * example 2): a process telling its own children what + * terminal they are on, which nothing else can see or + * contradict. + * + * It is still here only because DCL cannot yet reach + * /dev/vms in the runtime the CI harness can drive -- see + * the vms-d0b escalation. The replacement is not "pass a + * better variable": it is $ASSIGN to OPA0: and $GETDVI on + * the resulting channel. Do not build on this line. + */ setenv("VMS_TERMINAL", "_OPA0:", 1); /* Child: exec vms_login */ execl(loginout_path, "vms_login", (char *)NULL); diff --git a/src/vmsdcl/dcl_cmd_show.c b/src/vmsdcl/dcl_cmd_show.c index 76efa8ff0..68dbe55ad 100644 --- a/src/vmsdcl/dcl_cmd_show.c +++ b/src/vmsdcl/dcl_cmd_show.c @@ -537,6 +537,20 @@ static int cmd_show_protection(struct dcl_command *cmd) /* * SHOW DEVICE - List mounted filesystems as VMS devices. + * + * STOPGAP -- FACADE, NOT VMS (vms-d0b). This walks /proc/mounts and a + * process-local vms_device_table[], and where it finds nothing it + * prints a hardcoded stub row. On VMS, SHOW DEVICE is a READER of the + * executive's I/O database -- it cannot invent a device, and every + * process sees the same list. + * + * The executive now has that table (src/kernel/vms_devtab.c; + * $DEVICE_SCAN over it is vms_kif_devscan()). Converting this function + * to read it is blocked on DCL being buildable into the QEMU runtime -- + * see the vms-d0b escalation. Note also that no terminal appears in + * this listing at all today, though OPA0: is in the executive's table: + * see docs/oracle/vax73-terminal-device.md for the format VMS uses for + * a terminal row. */ static int cmd_show_device(struct dcl_command *cmd) { @@ -737,8 +751,20 @@ static int cmd_show_status(struct dcl_command *cmd) /* * SHOW TERMINAL - Display terminal characteristics. * - * Dynamically displays actual terminal state from the - * vms_terminal characteristics model. + * STOPGAP -- FACADE, NOT VMS (vms-d0b). This prints the DCL context's + * own copy of a terminal, which nothing outside this process wrote and + * nothing outside this process can see. On VMS, SHOW TERMINAL is a + * READER of the executive's device table (CLAUDE.md rule 11 + * corollary): it reports the characteristics the executive holds for + * the device, which is why what one process sets, another sees. + * + * The executive now has that table (src/kernel/vms_devtab.c, proven + * A-writes/B-reads by tests/qemu/test_kmod_devtab.c). Converting this + * function to read it is blocked on DCL being buildable into the QEMU + * runtime -- see the vms-d0b escalation. The characteristic list this + * prints also does not match the oracle: see + * docs/oracle/vax73-terminal-device.md for what VMS V7.3 actually + * displays. */ static int cmd_show_terminal(struct dcl_command *cmd) { diff --git a/src/vmsdcl/dcl_main.c b/src/vmsdcl/dcl_main.c index cd3e34fee..ec15ff61b 100644 --- a/src/vmsdcl/dcl_main.c +++ b/src/vmsdcl/dcl_main.c @@ -80,6 +80,26 @@ void dcl_context_init(struct dcl_context *ctx) /* SET TERMINAL defaults — full characteristics model */ vms_terminal_init(&ctx->terminal); + /* + * STOPGAP -- FACADE, NOT VMS (vms-d0b). Everything in this block + * decides what terminal this process is on by reading its own + * environment and, failing that, by handing itself a name out of a + * private pool. On VMS the answer comes from the executive's + * device table: $ASSIGN to the device, $GETDVI on the channel. A + * process cannot name its own terminal, which is exactly why a VMS + * terminal name means something to anyone else. + * + * As of vms-d0b the executive does have that table + * (src/kernel/vms_devtab.c: OPA0: exists at module init, carries an + * owner, a reference count and characteristics, and is readable by + * any process). This code has not been switched over to it because + * DCL is not yet built into the runtime where /dev/vms exists -- + * see the vms-d0b escalation. When it is switched over, SHOW + * TERMINAL and SHOW DEVICE become readers of the executive and + * these getenv() calls go away entirely; they are not to be + * "improved" in place. + */ + /* Terminal device type: set from VMS_DEVICE_TYPE env var if present * (set by vmssshd from SSH TERM negotiation) */ const char *env_devtype = getenv("VMS_DEVICE_TYPE"); diff --git a/src/vmsssh/vmssshd.c b/src/vmsssh/vmssshd.c index 66916c324..746858940 100644 --- a/src/vmsssh/vmssshd.c +++ b/src/vmsssh/vmssshd.c @@ -449,6 +449,18 @@ static void handle_connection(ssh_session session) /* Use SSH client's TERM value if provided, else default to vt100 */ setenv("TERM", ssh_term[0] ? ssh_term : "vt100", 1); + /* + * STOPGAP -- FACADE, NOT VMS (vms-d0b). VMS_DEVICE_TYPE and + * VMS_TERMINAL below hand a terminal identity to the session + * through the environment, which is the rejected VMS_PRCNAM + * shape (CLAUDE.md rule 10): the session ends up telling + * itself what terminal it is on. A VMS terminal is a device in + * the executive's device table (src/kernel/vms_devtab.c as of + * vms-d0b), assigned with $ASSIGN and read with $GETDVI. + * Remote terminals are explicitly out of scope for vms-d0b -- + * this site is marked, not fixed. + */ + /* Map SSH TERM to VMS device type */ const char *vms_devtype = map_term_to_vms_device_type( ssh_term[0] ? ssh_term : NULL); From 80cf76b395870c2c63e018adacf9fe0fa4a3ffa4 Mon Sep 17 00:00:00 2001 From: Chris Baron Date: Thu, 30 Jul 2026 09:55:04 -0400 Subject: [PATCH 3/8] vms-d0b: pin device ownership to the oracle -- $ALLOC owns, $ASSIGN does 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. --- docs/oracle/vax73-terminal-device.md | 137 ++++++++++++++- src/kernel/vms_devtab.c | 250 ++++++++++++++++++++++----- src/kernel/vms_internal.h | 44 ++++- src/kernel/vms_ioctl.h | 33 +++- src/kernel/vms_module.c | 4 + src/libvms/include/ssdef.h | 30 +++- src/libvms/status.c | 5 +- src/libvmssys/vms_kif.c | 29 ++++ src/libvmssys/vms_kif.h | 10 ++ tests/qemu/test_kmod_devtab.c | 115 ++++++++++-- 10 files changed, 587 insertions(+), 70 deletions(-) diff --git a/docs/oracle/vax73-terminal-device.md b/docs/oracle/vax73-terminal-device.md index 67bb54f4a..1817e81d8 100644 --- a/docs/oracle/vax73-terminal-device.md +++ b/docs/oracle/vax73-terminal-device.md @@ -139,6 +139,135 @@ $ SHOW DEVICE ZZA0: --- +## 7. Ownership, `$ASSIGN` and `$ALLOC` (captured 30-JUL-2026, node VAX2) + +This section exists because the first cut of `src/kernel/vms_devtab.c` asserted an ownership rule as +VMS fact with nothing behind it ("the first channel to an unowned device makes its holder the +owner"). It was measured rather than argued. Method: a **second process** was created on VAX2 with + +``` +$ RUN/DETACHED/INPUT=...DET.COM/OUTPUT=...DET.LOG/PROCESS_NAME=DEVPROBE SYS$SYSTEM:LOGINOUT.EXE +``` + +while the interactive job held the console, and it issued `$ASSIGN` through a MACRO-32 program +(`$ASSIGN_S DEVNAM=DEVDSC,CHAN=CHAN` followed by `LIB$SIGNAL` of R0, so VMS prints its own message). +No VSI binary was examined; this is the running system answering through its documented interfaces. + +### 7.1 `$ASSIGN` to a terminal another process owns SUCCEEDS + +`SHOW DEVICE/FULL OPA0:` as seen from the detached process — the console is owned by the interactive +job, PID `20400216`: + +``` +Terminal OPA0:, device type LA36, is online, record-oriented device, carriage + control, device is busy. + + Error count 0 Operations completed 293 + Owner process "SYSTEM" Owner UIC [SYSTEM] + Owner process ID 20400216 Dev Prot S:RWPL,O:RWPL,G,W + Reference count 2 Default buffer size 132 + +--- ASSIGN OPA0: FROM A SECOND PROCESS --- +%SYSTEM-S-NORMAL, normal successful completion +``` + +An RMS `OPEN/WRITE X OPA0:` from the same process also succeeded (`OPEN-OK`). + +### 7.2 `$ALLOC` in the same situation returns `SS$_DEVALLOC` + +Immediately afterwards, from that same detached process: + +``` +--- ALLOCATE OPA0: --- +%SYSTEM-W-DEVALLOC, device already allocated to another user +``` + +So `SS$_DEVALLOC` is `$ALLOC`'s condition, not `$ASSIGN`'s. **A terminal owned by another process is +assignable but not allocatable.** + +*Caveat recorded honestly:* `OPA0:`'s protection is `S:RWPL,O:RWPL,G,W` and the probing process ran +as `SYSTEM`. This capture therefore pins the *allocation* rule and says nothing about what an +unprivileged process gets — device protection is a separate gate OVMX does not implement. + +### 7.3 A channel does NOT confer ownership + +`NLA0:` before, during and after a channel was held by the observing process: + +``` +$ SHOW DEVICE/FULL NLA0: Owner process "" Owner process ID 00000000 Reference count 2 +$ OPEN/WRITE X NLA0: +$ SHOW DEVICE/FULL NLA0: Owner process "" Owner process ID 00000000 Reference count 3 +$ CLOSE X +$ SHOW DEVICE/FULL NLA0: Owner process "" Owner process ID 00000000 Reference count 2 +``` + +The owner fields never moved; only the reference count did. **Reference count is one per assigned +channel.** + +### 7.4 Foreign channels alone are enough to refuse `$ALLOC` + +With `NLA0:` unowned but at reference count 2 (channels held by other processes): + +``` +$ ALLOCATE NLA0: +%SYSTEM-W-DEVALLOC, device already allocated to another user +``` + +## 8. `ALLOCATE` sets the owner, adds a reference, and is idempotent + +On the interactive job, which already owned `OPA0:` but had not allocated it: + +``` +$ ALLOCATE OPA0: +%DCL-I-ALLOC, _VAX2$OPA0: allocated +$ SHOW DEVICE/FULL OPA0: +Terminal OPA0:, device type LA36, is online, allocated, ... Reference count 3 + +$ ALLOCATE OPA0: +%DCL-I-ALLOC, _VAX2$OPA0: allocated +$ SHOW DEVICE/FULL OPA0: +Terminal OPA0:, device type LA36, is online, allocated, ... Reference count 3 + +$ DEALLOCATE OPA0: +$ SHOW DEVICE/FULL OPA0: +Terminal OPA0:, device type LA36, is online, ... Reference count 2 + +$ DEALLOCATE OPA0: +%SYSTEM-W-DEVNOTALLOC, device not allocated +``` + +Three things are pinned here: allocation adds the word **`allocated`** to the status clause; +allocation is worth **one reference**; and re-allocating a device you already have allocated +succeeds and changes nothing. + +## 9. Condition values, from VMS's own message facility + +Asked directly, by scanning `F$MESSAGE(n)` on the running V7.3 system: + +``` + 312 %SYSTEM-W-IVCHAN, invalid I/O channel + 316 %SYSTEM-F-IVCHAN, invalid I/O channel + 320 %SYSTEM-W-IVDEVNAM, invalid device name + 324 %SYSTEM-F-IVDEVNAM, invalid device name + 2112 %SYSTEM-W-DEVALLOC, device already allocated to another user + 2116 %SYSTEM-F-DEVALLOC, device already allocated to another user + 2120 %SYSTEM-W-DEVASSIGN, device has channels assigned + 2136 %SYSTEM-W-DEVNOTALLOC, device not allocated + 2312 %SYSTEM-W-NOSUCHDEV, no such device available + 2316 %SYSTEM-F-NOSUCHDEV, no such device available + 2648 %SYSTEM-W-NOMOREDEV, no more devices + 2652 %SYSTEM-F-NOMOREDEV, no more devices +``` + +**This contradicts `src/libvms/include/ssdef.h` in several places.** The file's `SS$_NOMOREDEV` +(2648) is right; its `SS$_DEVALLOC` (2316), `SS$_NOSUCHDEV` (2680), `SS$_IVCHAN` (602) and +`SS$_IVDEVNAM` (608) are not. Only `SS$_DEVALLOC` was corrected as part of `vms-d0b` — it is the +constant this work introduces a use for, and it had no other consumer to break. The rest have a +blast radius across the kernel module, its client and its tests, and are tracked separately; do not +"fix" them without running the whole QEMU suite. + +--- + ## What OVMX took from this, and what it deliberately did not | Oracle fact | OVMX (`src/kernel/vms_devtab.c`) | @@ -148,7 +277,13 @@ $ SHOW DEVICE ZZA0: | Unidentified type displays `Unknown` | Console registers with device type 0 = Unknown | | Characteristic **names** and their two-state form | `VMS_TTC_*` in `src/kernel/vms_ioctl.h`, one bit per oracle name | | Absent device → `%SYSTEM-W-NOSUCHDEV` | `SS$_NOSUCHDEV` from `$ASSIGN`/`$GETDVI` | -| Owner / reference count are device properties | `owner_pid` / `refcnt` in the executive, released when the owner's last channel goes | +| `$ASSIGN` succeeds on a device another process owns (7.1) | `vms_ioctl_assign` returns `SS$_NORMAL` and does not touch ownership | +| Ownership comes from `$ALLOC`, never `$ASSIGN` (7.3, 8) | `vms_ioctl_alloc` sets `owner_pid`/`allocated`; `$ASSIGN` does not | +| `$ALLOC` of a device another process owns → `SS$_DEVALLOC` (7.2) | `vms_ioctl_alloc` returns `SS$_DEVALLOC` | +| `$ALLOC` refused while another process holds channels (7.4) | `vms_ioctl_alloc` walks `dev->chanlist` for a foreign holder | +| Re-`$ALLOC` by the owner succeeds, no extra reference (8) | idempotent branch in `vms_ioctl_alloc` | +| `$DALLOC` of an unallocated device → `SS$_DEVNOTALLOC` (8) | `vms_ioctl_dalloc` | +| Reference count = channels + allocation (7.3, 8) | `refcnt` in the executive | Deliberately **not** taken: diff --git a/src/kernel/vms_devtab.c b/src/kernel/vms_devtab.c index 799af5be1..e1639d8ec 100644 --- a/src/kernel/vms_devtab.c +++ b/src/kernel/vms_devtab.c @@ -190,6 +190,7 @@ static struct vms_device *vms_devtab_create(const char *devnam, dev->devchar = devchar; dev->width = width; dev->page = page; + INIT_LIST_HEAD(&dev->chanlist); spin_lock_init(&dev->lock); spin_lock(&vms_device_list_lock); @@ -242,33 +243,50 @@ static struct vms_channel *chan_find_locked(struct vms_proc *proc, uint32_t chan return NULL; } +/* Caller holds dev->lock. Clear an outstanding allocation. */ +static void device_dealloc_locked(struct vms_device *dev) +{ + if (!dev->allocated) + return; + dev->allocated = 0; + dev->owner_pid = 0; + dev->owner_linux_pid = 0; + dev->owner_uic = 0; + if (dev->refcnt > 0) + dev->refcnt--; +} + /* - * Drop one reference to a device. + * Give a channel back: unlink it from the device and drop the + * reference it held. * - * release_owner_pid, when non-zero, is a process that has just given - * up its LAST channel to this device: if it was the owner, the device - * becomes unowned even though other processes may still hold channels - * to it. Ownership on VMS belongs to a process that has the device; - * a process that has given it back -- or died -- does not still own - * it just because somebody else is using it. + * Note what this does NOT do: it does not touch ownership. Ownership + * is allocation, and allocation is released by $DALLOC or by the + * owner's death -- not by handing back a channel. That split is the + * oracle's, not a convenience: on the lab a process holding an open + * channel never became the device's owner in the first place. */ -static void device_deref(struct vms_device *dev, pid_t release_owner_pid) +static void device_release_channel(struct vms_channel *ch) { + struct vms_device *dev = ch->dev; + spin_lock(&dev->lock); + list_del(&ch->devlink); if (dev->refcnt > 0) dev->refcnt--; - if (dev->refcnt == 0 || - (release_owner_pid && dev->owner_linux_pid == release_owner_pid)) { - dev->owner_pid = 0; - dev->owner_linux_pid = 0; - dev->owner_uic = 0; - } spin_unlock(&dev->lock); } +/* + * Release everything a dying process held: its channels, and any + * device it had allocated. A device allocated by a process that no + * longer exists would be permanently unallocatable, which is not a + * state VMS has. + */ void vms_proc_release_channels(struct vms_proc *proc) { struct vms_channel *ch, *tmp; + struct vms_device *dev; LIST_HEAD(doomed); spin_lock(&proc->chan_lock); @@ -276,19 +294,52 @@ void vms_proc_release_channels(struct vms_proc *proc) list_move(&ch->list, &doomed); spin_unlock(&proc->chan_lock); - /* Every channel this process held is going, so its ownership of - * any device goes with them. */ list_for_each_entry_safe(ch, tmp, &doomed, list) { list_del(&ch->list); - device_deref(ch->dev, proc->linux_pid); + device_release_channel(ch); kfree(ch); } + + spin_lock(&vms_device_list_lock); + list_for_each_entry(dev, &vms_device_list, list) { + spin_lock(&dev->lock); + if (dev->allocated && dev->owner_linux_pid == proc->linux_pid) + device_dealloc_locked(dev); + spin_unlock(&dev->lock); + } + spin_unlock(&vms_device_list_lock); } /* ================================================================ * ioctl handlers * ================================================================ */ +/* + * $ASSIGN - take a channel to a device. + * + * ORACLE-PINNED SEMANTICS (docs/oracle/vax73-terminal-device.md + * section 7), because the obvious guesses are both wrong: + * + * - Assigning a channel does NOT make the caller the device's + * owner. On the lab an open channel to NLA0: left "Owner process" + * empty and "Owner process ID" 00000000 for as long as it was + * held. Ownership is $ALLOC's job. + * + * - $ASSIGN to a device another process owns SUCCEEDS. A detached + * process on the lab assigned a channel to OPA0: -- the console, + * owned by the interactive job -- and got + * %SYSTEM-S-NORMAL. SS$_DEVALLOC is what $ALLOC returns in that + * situation, not $ASSIGN; the same detached process got + * %SYSTEM-W-DEVALLOC from ALLOCATE OPA0: seconds later. + * + * NOT MODELLED, deliberately (rule 10 -- do not invent a handler for + * a condition we cannot pin): the lab's OPA0: carries a device + * protection mask of S:RWPL,O:RWPL,G,W, so a process outside the + * system UIC group would be refused by protection, not by allocation. + * The probe ran as SYSTEM, so it pins the allocated-device case and + * says nothing about the unprivileged case. OVMX has no device + * protection to check yet and therefore does not check one. + */ long vms_ioctl_assign(struct vms_proc *proc, unsigned long arg) { struct vms_assign_args args; @@ -323,21 +374,15 @@ long vms_ioctl_assign(struct vms_proc *proc, unsigned long arg) goto out; } + ch->dev = dev; + ch->owner_linux_pid = proc->linux_pid; + spin_lock(&dev->lock); - if (dev->owner_pid == 0) { - /* The first channel to an unowned device makes its holder the owner. */ - dev->owner_pid = proc->vms_pid; - dev->owner_linux_pid = proc->linux_pid; - dev->owner_uic = - (((uint32_t)from_kgid(&init_user_ns, current_gid()) & 0xFFFFu) << 16) | - ((uint32_t)from_kuid(&init_user_ns, current_uid()) & 0xFFFFu); - } dev->refcnt++; + list_add_tail(&ch->devlink, &dev->chanlist); spin_unlock(&dev->lock); spin_unlock(&vms_device_list_lock); - ch->dev = dev; - spin_lock(&proc->chan_lock); /* * Channel numbers are opaque to the caller on VMS ("the system @@ -362,7 +407,6 @@ long vms_ioctl_dassgn(struct vms_proc *proc, unsigned long arg) { struct vms_dassgn_args args; struct vms_channel *ch; - int last = 0; memset(&args, 0, sizeof(args)); if (copy_from_user(&args, (void __user *)arg, sizeof(args))) @@ -370,26 +414,14 @@ long vms_ioctl_dassgn(struct vms_proc *proc, unsigned long arg) spin_lock(&proc->chan_lock); ch = chan_find_locked(proc, args.chan); - if (ch) { - struct vms_channel *other; - + if (ch) list_del(&ch->list); - /* Does this process still hold another channel to the device? - * If not, it is giving the device up entirely. */ - last = 1; - list_for_each_entry(other, &proc->channels, list) { - if (other->dev == ch->dev) { - last = 0; - break; - } - } - } spin_unlock(&proc->chan_lock); if (!ch) { args.status = SS__IVCHAN; } else { - device_deref(ch->dev, last ? proc->linux_pid : 0); + device_release_channel(ch); kfree(ch); args.status = SS__NORMAL; } @@ -399,6 +431,139 @@ long vms_ioctl_dassgn(struct vms_proc *proc, unsigned long arg) return 0; } +/* + * $ALLOC - allocate a device to this process. + * + * This, not $ASSIGN, is what makes a process a device's owner. Every + * branch below is a case observed on the ~/vax OpenVMS VAX V7.3 lab + * (docs/oracle/vax73-terminal-device.md sections 7-9): + * + * allocated to another process -> %SYSTEM-W-DEVALLOC + * ALLOCATE OPA0: from a detached process while the interactive + * job held the console. + * another process holds channels -> %SYSTEM-W-DEVALLOC + * ALLOCATE NLA0: with "Owner process" empty and a reference + * count of 2 -- foreign channels alone are enough. + * already allocated to us -> success, reference count unchanged + * ALLOCATE OPA0: twice in a row: 2 -> 3 -> 3. + * otherwise -> success, reference count + 1 + * + * The caller's UIC is taken from its Linux credentials. That mapping + * is NOT oracle-pinned and is noted as such in vms-d0b's findings; it + * will have to agree with whatever replaces the VMS_UIC_* environment + * facade (vms-2b8). + */ +long vms_ioctl_alloc(struct vms_proc *proc, unsigned long arg) +{ + struct vms_alloc_args args; + struct vms_device *dev; + struct vms_channel *ch; + char devnam[VMS_DEVNAM_SIZE]; + uint32_t status; + int foreign = 0; + + memset(&args, 0, sizeof(args)); + if (copy_from_user(&args, (void __user *)arg, sizeof(args))) + return -EFAULT; + args.devnam[VMS_DEVNAM_SIZE - 1] = '\0'; + + status = normalize_devnam(args.devnam, devnam, sizeof(devnam)); + if (status != SS__NORMAL) { + args.status = status; + goto out; + } + + spin_lock(&vms_device_list_lock); + dev = devtab_lookup_locked(devnam); + if (!dev) { + spin_unlock(&vms_device_list_lock); + args.status = SS__NOSUCHDEV; + goto out; + } + + spin_lock(&dev->lock); + if (dev->allocated && dev->owner_linux_pid == proc->linux_pid) { + args.status = SS__NORMAL; /* already ours; idempotent */ + } else if (dev->allocated) { + args.status = SS__DEVALLOC; + } else { + list_for_each_entry(ch, &dev->chanlist, devlink) { + if (ch->owner_linux_pid != proc->linux_pid) { + foreign = 1; + break; + } + } + if (foreign) { + args.status = SS__DEVALLOC; + } else { + dev->allocated = 1; + dev->owner_pid = proc->vms_pid; + dev->owner_linux_pid = proc->linux_pid; + dev->owner_uic = + (((uint32_t)from_kgid(&init_user_ns, current_gid()) & 0xFFFFu) << 16) | + ((uint32_t)from_kuid(&init_user_ns, current_uid()) & 0xFFFFu); + dev->refcnt++; + args.status = SS__NORMAL; + } + } + spin_unlock(&dev->lock); + spin_unlock(&vms_device_list_lock); + +out: + if (copy_to_user((void __user *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} + +/* + * $DALLOC - give an allocated device back. + * + * Deallocating a device this process does not have allocated is + * %SYSTEM-W-DEVNOTALLOC on the lab (a second DEALLOCATE OPA0: right + * after the first). + */ +long vms_ioctl_dalloc(struct vms_proc *proc, unsigned long arg) +{ + struct vms_alloc_args args; + struct vms_device *dev; + char devnam[VMS_DEVNAM_SIZE]; + uint32_t status; + + memset(&args, 0, sizeof(args)); + if (copy_from_user(&args, (void __user *)arg, sizeof(args))) + return -EFAULT; + args.devnam[VMS_DEVNAM_SIZE - 1] = '\0'; + + status = normalize_devnam(args.devnam, devnam, sizeof(devnam)); + if (status != SS__NORMAL) { + args.status = status; + goto out; + } + + spin_lock(&vms_device_list_lock); + dev = devtab_lookup_locked(devnam); + if (!dev) { + spin_unlock(&vms_device_list_lock); + args.status = SS__NOSUCHDEV; + goto out; + } + + spin_lock(&dev->lock); + if (dev->allocated && dev->owner_linux_pid == proc->linux_pid) { + device_dealloc_locked(dev); + args.status = SS__NORMAL; + } else { + args.status = SS__DEVNOTALLOC; + } + spin_unlock(&dev->lock); + spin_unlock(&vms_device_list_lock); + +out: + if (copy_to_user((void __user *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} + /* Snapshot a device row for userspace. Takes dev->lock. */ static void devinfo_fill(struct vms_device *dev, struct vms_devinfo *info) { @@ -410,6 +575,7 @@ static void devinfo_fill(struct vms_device *dev, struct vms_devinfo *info) info->devtype = dev->devtype; info->owner_pid = dev->owner_pid; info->owner_uic = dev->owner_uic; + info->allocated = dev->allocated; info->refcnt = dev->refcnt; info->errcnt = dev->errcnt; info->opcnt = dev->opcnt; diff --git a/src/kernel/vms_internal.h b/src/kernel/vms_internal.h index e8d5761c9..12b7cfcdc 100644 --- a/src/kernel/vms_internal.h +++ b/src/kernel/vms_internal.h @@ -58,6 +58,18 @@ #define SS__IVDEVNAM 608 /* invalid device name */ #define SS__NOMOREDEV 2648 /* device scan exhausted */ #define SS__NOSUCHDEV 2680 /* no such device available */ +/* + * Allocation statuses. Unlike the four above, these two were measured + * directly on the oracle rather than inherited: VMS's own message + * facility on the ~/vax OpenVMS VAX V7.3 lab reports + * 2112 %SYSTEM-W-DEVALLOC, device already allocated to another user + * 2136 %SYSTEM-W-DEVNOTALLOC, device not allocated + * and $ALLOC/$DALLOC were observed returning exactly those conditions + * (docs/oracle/vax73-terminal-device.md sections 7-9). ssdef.h carries + * the same values and the same citation. + */ +#define SS__DEVALLOC 2112 /* device already allocated to another user */ +#define SS__DEVNOTALLOC 2136 /* device not allocated */ /* * Default privilege set for non-CAP_SYS_ADMIN processes. @@ -199,12 +211,21 @@ struct vms_device { uint32_t devtype; /* device type code; 0 = Unknown */ /* - * Ownership. owner_pid is the VMS pid of the process that first - * took a channel to the device and is held until the LAST channel - * is given back -- SHOW DEVICE/FULL's "Owner process ID" together - * with "Reference count". + * Ownership comes from ALLOCATION, not from assigning a channel. + * This is measured, not assumed: on the ~/vax OpenVMS VAX V7.3 lab + * a process that held an open channel to NLA0: left the device's + * "Owner process" empty and its "Owner process ID" 00000000, while + * ALLOCATE set both and added the word "allocated" to the + * SHOW DEVICE/FULL status clause + * (docs/oracle/vax73-terminal-device.md sections 7-9). + * + * refcnt is the device's "Reference count": one per assigned + * channel plus one for an outstanding allocation -- also measured + * (NLA0: 2 -> 3 -> 2 across an OPEN/CLOSE; OPA0: 2 -> 3 on + * ALLOCATE and back to 2 on DEALLOCATE). */ - uint32_t owner_pid; + uint32_t allocated; /* 1 while allocated to owner_* */ + uint32_t owner_pid; /* VMS pid of the allocating process */ pid_t owner_linux_pid; uint32_t owner_uic; uint32_t refcnt; @@ -217,13 +238,24 @@ struct vms_device { uint32_t width; uint32_t page; + /* + * Every channel currently assigned to this device, by any process. + * The device has to know this to answer $ALLOC: the oracle refuses + * to allocate a device that another process merely has channels to + * (ALLOCATE NLA0: -> %SYSTEM-W-DEVALLOC with the owner field still + * empty and a reference count of 2). + */ + struct list_head chanlist; /* of vms_channel.devlink */ + spinlock_t lock; }; /* A process's handle on a device. */ struct vms_channel { struct list_head list; /* in vms_proc->channels */ + struct list_head devlink; /* in vms_device->chanlist */ uint32_t chan; + pid_t owner_linux_pid;/* process holding this channel */ struct vms_device *dev; }; @@ -296,6 +328,8 @@ long vms_ioctl_dassgn(struct vms_proc *proc, unsigned long arg); long vms_ioctl_getdvi(struct vms_proc *proc, unsigned long arg); long vms_ioctl_devscan(struct vms_proc *proc, unsigned long arg); long vms_ioctl_ttsetmode(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_alloc(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_dalloc(struct vms_proc *proc, unsigned long arg); /* Subsystem init/cleanup */ int vms_lock_init(void); diff --git a/src/kernel/vms_ioctl.h b/src/kernel/vms_ioctl.h index 78df27186..dccfe6952 100644 --- a/src/kernel/vms_ioctl.h +++ b/src/kernel/vms_ioctl.h @@ -315,7 +315,14 @@ struct vms_register_args { /* * One row of the executive device table, as handed to userspace. * - * owner_pid is 0 when the device is unowned. opcnt/errcnt are the + * owner_pid is 0 when the device is not allocated -- ownership comes + * from $ALLOC, never from $ASSIGN (measured on the oracle; see + * docs/oracle/vax73-terminal-device.md section 7). `allocated` is the + * flag behind the word "allocated" in SHOW DEVICE/FULL's status + * clause. refcnt is the "Reference count": one per assigned channel + * plus one for an outstanding allocation. + * + * opcnt/errcnt are the * "Operations completed" and "Error count" SHOW DEVICE/FULL reports. * * NOTE what is deliberately ABSENT (rule 10 -- hide what we cannot @@ -329,14 +336,32 @@ struct vms_devinfo { char devnam[VMS_DEVNAM_SIZE]; /* physical name, e.g. "OPA0:" */ uint32_t devclass; /* DC$_ device class */ uint32_t devtype; /* device type code; 0 = Unknown */ - uint32_t owner_pid; /* VMS pid of the owner, 0 = unowned */ + uint32_t owner_pid; /* VMS pid of the owner, 0 = not allocated */ uint32_t owner_uic; /* (group << 16) | member */ - uint32_t refcnt; /* channels currently assigned */ + uint32_t refcnt; /* channels assigned + allocation */ uint32_t errcnt; /* Error count */ uint64_t opcnt; /* Operations completed */ uint64_t devchar; /* VMS_TTC_* (terminals only) */ uint32_t width; /* terminal width */ uint32_t page; /* terminal page length */ + uint32_t allocated; /* 1 = allocated to owner_pid */ + uint32_t pad; +}; + +/* + * $ALLOC / $DALLOC: allocate a device to this process, and give it + * back. This is what makes a process the device's OWNER; $ASSIGN does + * not (oracle, docs/oracle/vax73-terminal-device.md section 7). + * + * $ALLOC returns SS$_DEVALLOC when the device is allocated to another + * process, or when another process merely holds channels to it -- both + * observed on the lab. $DALLOC returns SS$_DEVNOTALLOC when this + * process does not have it allocated. + */ +struct vms_alloc_args { + char devnam[VMS_DEVNAM_SIZE]; + uint32_t status; + uint32_t pad; }; /* $ASSIGN: take a channel to a device by name. */ @@ -403,5 +428,7 @@ struct vms_setmode_args { #define VMS_IOCTL_GETDVI _IOWR(VMS_IOC_MAGIC, 0x52, struct vms_getdvi_args) #define VMS_IOCTL_DEVSCAN _IOWR(VMS_IOC_MAGIC, 0x53, struct vms_devscan_args) #define VMS_IOCTL_TTSETMODE _IOWR(VMS_IOC_MAGIC, 0x54, struct vms_setmode_args) +#define VMS_IOCTL_ALLOC _IOWR(VMS_IOC_MAGIC, 0x55, struct vms_alloc_args) +#define VMS_IOCTL_DALLOC _IOWR(VMS_IOC_MAGIC, 0x56, struct vms_alloc_args) #endif /* _VMS_IOCTL_H */ diff --git a/src/kernel/vms_module.c b/src/kernel/vms_module.c index 8fa6ecbde..9aea0d9ff 100644 --- a/src/kernel/vms_module.c +++ b/src/kernel/vms_module.c @@ -270,6 +270,10 @@ static long vms_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg return vms_ioctl_devscan(proc, arg); case VMS_IOCTL_TTSETMODE: return vms_ioctl_ttsetmode(proc, arg); + case VMS_IOCTL_ALLOC: + return vms_ioctl_alloc(proc, arg); + case VMS_IOCTL_DALLOC: + return vms_ioctl_dalloc(proc, arg); default: return -ENOTTY; diff --git a/src/libvms/include/ssdef.h b/src/libvms/include/ssdef.h index a878f89c6..a32012448 100644 --- a/src/libvms/include/ssdef.h +++ b/src/libvms/include/ssdef.h @@ -221,7 +221,35 @@ extern "C" { /* Additional status codes */ #define SS$_FILACCERR 2312 /* File access error */ -#define SS$_DEVALLOC 2316 /* Device already allocated */ +/* + * SS$_DEVALLOC / SS$_DEVNOTALLOC. + * + * PROVENANCE: measured on the ~/vax OpenVMS VAX V7.3 lab (node VAX2, + * 30-JUL-2026) by asking VMS's own message facility for the text of + * each condition value -- `WRITE SYS$OUTPUT F$MESSAGE(n)` -- and + * scanning for the name. VMS answered: + * 2112 %SYSTEM-W-DEVALLOC, device already allocated to another user + * 2116 %SYSTEM-F-DEVALLOC, device already allocated to another user + * 2136 %SYSTEM-W-DEVNOTALLOC, device not allocated + * 2140 %SYSTEM-F-DEVNOTALLOC, device not allocated + * and the behaviour was confirmed end to end: `ALLOCATE OPA0:` issued + * from a second (detached) process while the interactive job held the + * console printed exactly + * %SYSTEM-W-DEVALLOC, device already allocated to another user + * and a second `DEALLOCATE` of an already-deallocated device printed + * %SYSTEM-W-DEVNOTALLOC, device not allocated + * (docs/oracle/vax73-terminal-device.md sections 7-9). The warning + * form is the one $SSDEF carries, so that is the value used here. + * + * The previous value on this line, 2316, was wrong: the same probe + * shows 2316 is %SYSTEM-F-NOSUCHDEV. That measurement also disagrees + * with several OTHER values in this file (see the note on + * SS$_NOSUCHDEV above); correcting the rest has a blast radius across + * the kernel module and its tests and is tracked separately, not done + * here. + */ +#define SS$_DEVALLOC 2112 /* Device already allocated to another user */ +#define SS$_DEVNOTALLOC 2136 /* Device not allocated */ #define SS$_IVLOGTAB 2320 /* Invalid logical name table */ #define SS$_NOLOGTAB 2324 /* No such logical name table */ diff --git a/src/libvms/status.c b/src/libvms/status.c index edafb9d62..6e47f5c63 100644 --- a/src/libvms/status.c +++ b/src/libvms/status.c @@ -57,7 +57,10 @@ static const struct status_entry known_codes[] = { { SS$_FILACCERR, "SYSTEM", "FILACCERR", "file access error" }, { SS$_DEVMOUNT, "SYSTEM", "DEVMOUNT", "device already mounted" }, { SS$_DEVNOTMOUNT, "SYSTEM", "DEVNOTMOUNT", "device not mounted" }, - { SS$_DEVALLOC, "SYSTEM", "DEVALLOC", "device already allocated" }, + /* Message text measured on the ~/vax OpenVMS VAX V7.3 lab via + * F$MESSAGE -- see the provenance note in ssdef.h. */ + { SS$_DEVALLOC, "SYSTEM", "DEVALLOC", "device already allocated to another user" }, + { SS$_DEVNOTALLOC, "SYSTEM", "DEVNOTALLOC", "device not allocated" }, { SS$_IVDEVNAM, "SYSTEM", "IVDEVNAM", "invalid device name" }, { SS$_IVLOGNAM, "SYSTEM", "IVLOGNAM", "invalid logical name" }, { SS$_IVLOGTAB, "SYSTEM", "IVLOGTAB", "invalid logical name table" }, diff --git a/src/libvmssys/vms_kif.c b/src/libvmssys/vms_kif.c index 881095d08..4fb9b8088 100644 --- a/src/libvmssys/vms_kif.c +++ b/src/libvmssys/vms_kif.c @@ -408,6 +408,35 @@ uint32_t vms_kif_assign(const char *devnam, uint32_t *chan) return args.status; } +/* Shared body for $ALLOC and $DALLOC: both name a device and return a + * status, and neither writes anything back to the caller. */ +static uint32_t vms_kif_alloc_op(unsigned long req, const char *devnam) +{ + struct vms_alloc_args args; + + if (!devnam) + return 0x00000014; /* SS$_BADPARAM */ + + vms_memset(&args, 0, sizeof(args)); + vms_strncpy(args.devnam, devnam, VMS_DEVNAM_SIZE - 1); + args.devnam[VMS_DEVNAM_SIZE - 1] = '\0'; + + if (vms_sys_ioctl(vms_dev_fd, req, (unsigned long)&args) < 0) + return 0x00000014; + + return args.status; +} + +uint32_t vms_kif_alloc(const char *devnam) +{ + return vms_kif_alloc_op(VMS_IOCTL_ALLOC, devnam); +} + +uint32_t vms_kif_dalloc(const char *devnam) +{ + return vms_kif_alloc_op(VMS_IOCTL_DALLOC, devnam); +} + uint32_t vms_kif_dassgn(uint32_t chan) { struct vms_dassgn_args args; diff --git a/src/libvmssys/vms_kif.h b/src/libvmssys/vms_kif.h index 826abc4b1..f8213689b 100644 --- a/src/libvmssys/vms_kif.h +++ b/src/libvmssys/vms_kif.h @@ -128,6 +128,16 @@ uint32_t vms_kif_assign(const char *devnam, uint32_t *chan); /* $DASSGN the channel. SS$_IVCHAN if it is not one of ours. */ uint32_t vms_kif_dassgn(uint32_t chan); +/* $ALLOC the device to this process -- this, and not $ASSIGN, is what + * makes a process the device's owner. SS$_DEVALLOC when it is already + * allocated to another process or another process holds channels to + * it; SS$_NOSUCHDEV when there is no such device. */ +uint32_t vms_kif_alloc(const char *devnam); + +/* $DALLOC the device. SS$_DEVNOTALLOC if this process does not have it + * allocated. */ +uint32_t vms_kif_dalloc(const char *devnam); + /* Read a device row by name. SS$_NOSUCHDEV if there is no such device. */ uint32_t vms_kif_getdvi_devnam(const char *devnam, struct vms_devinfo *info); diff --git a/tests/qemu/test_kmod_devtab.c b/tests/qemu/test_kmod_devtab.c index 79047b61c..268ab7bad 100644 --- a/tests/qemu/test_kmod_devtab.c +++ b/tests/qemu/test_kmod_devtab.c @@ -40,6 +40,8 @@ #define SS_IVDEVNAM 608 #define SS_NOMOREDEV 2648 #define SS_NOSUCHDEV 2680 +#define SS_DEVALLOC 2112 /* oracle-measured; see ssdef.h provenance */ +#define SS_DEVNOTALLOC 2136 #define DC_TERM 6 @@ -61,9 +63,14 @@ static int pass = 0, fail = 0; struct owner_report { uint32_t assign_status; uint32_t setmode_status; + uint32_t alloc_status; + uint32_t realloc_status; /* allocating again, already ours */ uint32_t chan; - uint32_t owner_pid; /* owner the executive shows A after assign */ - uint32_t refcnt; + uint32_t owner_after_assign;/* owner the executive shows after $ASSIGN alone */ + uint32_t refcnt_after_assign; + uint32_t owner_pid; /* owner after $ALLOC */ + uint32_t refcnt; /* reference count after $ALLOC */ + uint32_t allocated; }; static int open_and_register(void) @@ -80,7 +87,8 @@ static int open_and_register(void) } /* - * process_a - assigns a channel to the console, becomes its owner, and + * process_a - assigns a channel to the console, allocates it (which is + * what makes it the owner -- $ASSIGN does not, per the oracle), and * changes characteristics that it never tells anyone about except * through the executive. */ @@ -102,6 +110,18 @@ static int process_a(int wfd) rep.assign_status = vms_kif_assign(CONSOLE, &rep.chan); if (rep.assign_status == SS_NORMAL) { + /* After $ASSIGN alone: the oracle says we are NOT the owner. */ + memset(&info, 0, sizeof(info)); + if (vms_kif_getdvi_chan(rep.chan, &info) == SS_NORMAL) { + rep.owner_after_assign = info.owner_pid; + rep.refcnt_after_assign = info.refcnt; + } + + rep.alloc_status = vms_kif_alloc(CONSOLE); + /* Allocating a device we already have is a no-op on the lab, + * including its reference count. */ + rep.realloc_status = vms_kif_alloc(CONSOLE); + rep.setmode_status = vms_kif_ttsetmode( rep.chan, VMS_TTSET_CHAR | VMS_TTSET_WIDTH | VMS_TTSET_PAGE, @@ -113,6 +133,7 @@ static int process_a(int wfd) if (vms_kif_getdvi_chan(rep.chan, &info) == SS_NORMAL) { rep.owner_pid = info.owner_pid; rep.refcnt = info.refcnt; + rep.allocated = info.allocated; } } @@ -231,15 +252,34 @@ int main(int argc, char **argv) CHECK(rep.assign_status == SS_NORMAL, "another process assigns a channel to OPA0:"); CHECK(rep.setmode_status == SS_NORMAL, "owner sets terminal characteristics"); - CHECK(rep.owner_pid == (uint32_t)child, "executive records the assigning process as owner"); - CHECK(rep.refcnt == 1, "executive counts one channel to the device"); + + /* ORACLE (VAX 7.3, NLA0: held open by one process): a channel does + * not confer ownership. The owner field stayed empty and the + * process ID stayed 00000000 for as long as the channel was held; + * only the reference count moved. */ + CHECK(rep.owner_after_assign == 0, + "$ASSIGN alone does not make the caller the device's owner"); + CHECK(rep.refcnt_after_assign == 1, + "$ASSIGN alone adds one to the reference count"); + + /* ORACLE: ALLOCATE is what sets Owner process / Owner process ID + * and adds the word "allocated" to SHOW DEVICE/FULL, and doing it + * twice changes nothing further (OPA0: 2 -> 3 -> 3). */ + CHECK(rep.alloc_status == SS_NORMAL, "$ALLOC of a free device succeeds"); + CHECK(rep.realloc_status == SS_NORMAL, + "$ALLOC of a device we already have allocated succeeds"); + CHECK(rep.owner_pid == (uint32_t)child, "executive records the allocating process as owner"); + CHECK(rep.allocated == 1, "device reports itself allocated"); + CHECK(rep.refcnt == 2, + "reference count is one channel plus the allocation, and re-allocating adds none"); memset(&info, 0, sizeof(info)); status = vms_kif_getdvi_devnam(CONSOLE, &info); CHECK(status == SS_NORMAL, "this process can still read the device"); CHECK(info.owner_pid == (uint32_t)child, "B sees the owner A took (A writes, B reads)"); - CHECK(info.refcnt == 1, "B sees A's reference count"); + CHECK(info.allocated == 1, "B sees that A allocated the device"); + CHECK(info.refcnt == 2, "B sees A's reference count"); CHECK(info.width == A_WIDTH && info.page == A_PAGE, "B sees the width and page A set"); CHECK((info.devchar & VMS_TTC_PASTHRU) != 0, @@ -248,29 +288,56 @@ int main(int argc, char **argv) "B sees the characteristic A cleared (No Echo)"); /* -------------------------------------------------------------- - * 4. B assigns the same device: the device is shared, and B does - * NOT displace A as its owner. + * 4. What a SECOND process may and may not do to a device another + * process owns. Both answers are measured on the ~/vax OpenVMS + * VAX V7.3 lab, not chosen (docs/oracle/vax73-terminal-device.md + * section 7): + * + * a detached process, $ASSIGN OPA0: -> %SYSTEM-S-NORMAL + * the same process, ALLOCATE OPA0: -> %SYSTEM-W-DEVALLOC, + * device already allocated to another user + * + * So a terminal owned by somebody else is assignable but not + * allocatable. Neither line below is OVMX's opinion. * -------------------------------------------------------------- */ status = vms_kif_assign(CONSOLE, &chan); - CHECK(status == SS_NORMAL && chan != 0, "B assigns its own channel to the same device"); + CHECK(status == SS_NORMAL && chan != 0, + "oracle: $ASSIGN to a device another process owns returns SS$_NORMAL"); + + status = vms_kif_alloc(CONSOLE); + CHECK(status == SS_DEVALLOC, + "oracle: $ALLOC of a device another process owns returns SS$_DEVALLOC"); memset(&info, 0, sizeof(info)); status = vms_kif_getdvi_chan(chan, &info); - CHECK(status == SS_NORMAL && info.refcnt == 2, - "device reference count counts both processes"); + CHECK(status == SS_NORMAL && info.refcnt == 3, + "device reference count counts both processes' channels and the allocation"); CHECK(info.owner_pid == (uint32_t)child, - "a second assigner does not steal ownership"); + "a refused $ALLOC leaves the existing owner in place"); + + /* Deallocating something we never allocated is DEVNOTALLOC, as it + * is on the lab for a second DEALLOCATE. */ + status = vms_kif_dalloc(CONSOLE); + CHECK(status == SS_DEVNOTALLOC, + "oracle: $DALLOC of a device we do not have allocated returns SS$_DEVNOTALLOC"); + memset(&info, 0, sizeof(info)); + (void)vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(info.owner_pid == (uint32_t)child, + "a refused $DALLOC does not release somebody else's allocation"); /* A failed $ASSIGN must not disturb the channel we already hold. */ bogus_chan = chan; status = vms_kif_assign(ABSENT_DEV, &bogus_chan); CHECK(status == SS_NOSUCHDEV, "assigning an absent device fails with SS$_NOSUCHDEV"); CHECK(bogus_chan == chan, "failed $ASSIGN leaves the caller's channel untouched"); + status = vms_kif_alloc(ABSENT_DEV); + CHECK(status == SS_NOSUCHDEV, "allocating an absent device fails with SS$_NOSUCHDEV"); /* -------------------------------------------------------------- * 5. The device outlives its owner. When A dies the executive - * takes the ownership back -- the device is the executive's, - * not A's. + * takes the allocation back -- the device is the executive's, + * not A's. (A device left allocated to a process that no longer + * exists is not a state VMS has.) * -------------------------------------------------------------- */ kill(child, SIGKILL); waitpid(child, NULL, 0); @@ -279,13 +346,27 @@ int main(int argc, char **argv) status = vms_kif_getdvi_devnam(CONSOLE, &info); CHECK(status == SS_NORMAL, "device still exists after its owner dies"); CHECK(info.owner_pid == 0, "dead process no longer owns the device"); - CHECK(info.refcnt == 1, "dead process's channel was released"); + CHECK(info.allocated == 0, "dead process's allocation was released"); + CHECK(info.refcnt == 1, "dead process's channel and allocation were both released"); CHECK(info.width == A_WIDTH && info.page == A_PAGE, "characteristics set by the dead process persist in the executive"); + /* Now that A is gone the device is free, so B can take it. */ + status = vms_kif_alloc(CONSOLE); + CHECK(status == SS_NORMAL, "device is allocatable again once its owner is gone"); + memset(&info, 0, sizeof(info)); + (void)vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(info.owner_pid == (uint32_t)getpid() && info.allocated == 1, + "executive records the new owner"); + status = vms_kif_dalloc(CONSOLE); + CHECK(status == SS_NORMAL, "$DALLOC gives the device back"); + memset(&info, 0, sizeof(info)); + (void)vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(info.owner_pid == 0 && info.allocated == 0 && info.refcnt == 1, + "$DALLOC clears the owner and drops its reference"); + /* -------------------------------------------------------------- - * 6. Giving a channel back releases the reference and, with it, - * ownership of the device. + * 6. Giving a channel back releases the reference. * -------------------------------------------------------------- */ status = vms_kif_dassgn(chan); CHECK(status == SS_NORMAL, "channel deassigned"); From 09bb7737f5e036f8bcc5faf201177f5a127bcd08 Mon Sep 17 00:00:00 2001 From: Chris Baron Date: Thu, 30 Jul 2026 09:55:16 -0400 Subject: [PATCH 4/8] vms-d0b: terminal.h's TT_* bits are OVMX's, not VMS TT$ constants 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. --- src/vmsdcl/include/dcl/terminal.h | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/vmsdcl/include/dcl/terminal.h b/src/vmsdcl/include/dcl/terminal.h index 3d7cca3ab..e563f7b22 100644 --- a/src/vmsdcl/include/dcl/terminal.h +++ b/src/vmsdcl/include/dcl/terminal.h @@ -14,7 +14,32 @@ #include #include -/* Terminal characteristic bits (matching VMS TT$ constants) */ +/* + * Terminal characteristic bits. + * + * THESE ARE OVMX-DEFINED VALUES, NOT VMS $TTDEF TT$ CONSTANTS. The + * line that used to be here claimed they matched VMS's; they do not. + * Neither the bit positions nor, in several cases, the characteristic + * NAMES correspond to OpenVMS: the display table in + * src/vmsdcl/dcl_terminal.c carries Scope, Holdscreen, Mechtab, Oper, + * Page, Runout and AltTypeAhd, none of which appear in the SHOW + * TERMINAL output of OpenVMS VAX V7.3, and it omits most of the names + * that do (docs/oracle/vax73-terminal-device.md section 2 has the real + * list, captured verbatim from the ~/vax lab). + * + * CLAUDE.md rule 8 permits OVMX to define its own representation where + * the public documentation publishes no byte-level layout -- which is + * the case for $TTDEF -- but requires it to be LABELLED as an OVMX + * design choice rather than presented as VMS-authentic. This comment + * is that label. Nothing may assume these values or names match VMS. + * + * The executive's own characteristic vector (VMS_TTC_* in + * src/kernel/vms_ioctl.h) is the oracle-pinned one: its NAMES are + * exactly the V7.3 set, with only the bit positions OVMX's own. + * Reconciling this header and dcl_terminal.c's char_display[] against + * that vector is tracked separately -- it is a change to user-visible + * SET/SHOW TERMINAL output, not a comment fix. + */ #define TT_ECHO (1U << 0) #define TT_WRAP (1U << 1) #define TT_BROADCAST (1U << 2) From ee9ff6681ce04b8da8ca141a584cacb773e7e08f Mon Sep 17 00:00:00 2001 From: Chris Baron Date: Thu, 30 Jul 2026 10:35:48 -0400 Subject: [PATCH 5/8] vms-d0b: ownership is what the oracle says it is -- a channel to a non-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 --- src/kernel/vms_devtab.c | 212 ++++++++++++++++++++++++++------------ src/kernel/vms_internal.h | 53 +++++++--- src/kernel/vms_ioctl.h | 42 +++++--- 3 files changed, 213 insertions(+), 94 deletions(-) diff --git a/src/kernel/vms_devtab.c b/src/kernel/vms_devtab.c index e1639d8ec..1ef3405e4 100644 --- a/src/kernel/vms_devtab.c +++ b/src/kernel/vms_devtab.c @@ -175,6 +175,7 @@ static struct vms_device *devtab_lookup_locked(const char *devnam) static struct vms_device *vms_devtab_create(const char *devnam, uint32_t devclass, uint32_t devtype, + uint32_t shareable, uint64_t devchar, uint32_t width, uint32_t page) { @@ -185,11 +186,12 @@ static struct vms_device *vms_devtab_create(const char *devnam, return NULL; strscpy(dev->devnam, devnam, sizeof(dev->devnam)); - dev->devclass = devclass; - dev->devtype = devtype; - dev->devchar = devchar; - dev->width = width; - dev->page = page; + dev->devclass = devclass; + dev->devtype = devtype; + dev->shareable = shareable; + dev->devchar = devchar; + dev->width = width; + dev->page = page; INIT_LIST_HEAD(&dev->chanlist); spin_lock_init(&dev->lock); @@ -204,7 +206,14 @@ int vms_devtab_init(void) { struct vms_device *console; + /* + * shareable = 0: the oracle's terminals are not shareable. Neither + * "Terminal OPA0: ..." nor "Terminal TTA0: ..." carries the word + * "shareable" in SHOW DEVICE/FULL's status clause, where NLA0: and + * the MBAn: mailboxes do. + */ console = vms_devtab_create(VMS_CONSOLE_DEVNAM, DC__TERM, VMS_DT_UNKNOWN, + 0 /* shareable */, VMS_CONSOLE_DEVCHAR, VMS_CONSOLE_WIDTH, VMS_CONSOLE_PAGE); if (!console) @@ -243,45 +252,99 @@ static struct vms_channel *chan_find_locked(struct vms_proc *proc, uint32_t chan return NULL; } -/* Caller holds dev->lock. Clear an outstanding allocation. */ -static void device_dealloc_locked(struct vms_device *dev) +/* + * The caller's UIC, from its Linux credentials. + * + * NOT ORACLE-PINNED, and recorded as such in vms-d0b's findings: it is + * OVMX's own mapping and no test asserts it. It will have to agree with + * whatever replaces the VMS_UIC_* environment facade (vms-2b8). + */ +static uint32_t caller_uic(void) { - if (!dev->allocated) + return (((uint32_t)from_kgid(&init_user_ns, current_gid()) & 0xFFFFu) << 16) | + ((uint32_t)from_kuid(&init_user_ns, current_uid()) & 0xFFFFu); +} + +/* Caller holds dev->lock. Does `pid` still hold a channel to this device? */ +static int device_has_channel_locked(struct vms_device *dev, pid_t pid) +{ + struct vms_channel *ch; + + list_for_each_entry(ch, &dev->chanlist, devlink) { + if (ch->owner_linux_pid == pid) + return 1; + } + return 0; +} + +/* + * Caller holds dev->lock. End ownership that rests on nothing but a + * channel. + * + * ORACLE (VAX 7.3, non-shareable terminal TTA0:): a bare OPEN/WRITE + * made the opener the owner; CLOSE put the device back to + * Owner "" / reference count 0. An ALLOCATION is different and is not + * touched here -- DEALLOCATE OPA0: left the still-channel-holding job + * as Owner "SYSTEM" with the reference count going 3 -> 2, so the + * allocation is what ends, not the ownership. + */ +static void device_release_implicit_owner_locked(struct vms_device *dev, pid_t pid) +{ + if (dev->allocated) return; - dev->allocated = 0; + if (pid == 0 || dev->owner_linux_pid != pid) + return; + if (device_has_channel_locked(dev, pid)) + return; + dev->owner_pid = 0; dev->owner_linux_pid = 0; dev->owner_uic = 0; +} + +/* + * Caller holds dev->lock. Give an allocation back: the device stops + * being "allocated" and loses the reference the allocation held. What + * happens to ownership afterwards is the implicit rule above -- the + * ex-allocator keeps the device while it still holds a channel. + */ +static void device_dealloc_locked(struct vms_device *dev) +{ + pid_t owner = dev->owner_linux_pid; + + if (!dev->allocated) + return; + dev->allocated = 0; if (dev->refcnt > 0) dev->refcnt--; + device_release_implicit_owner_locked(dev, owner); } /* - * Give a channel back: unlink it from the device and drop the - * reference it held. - * - * Note what this does NOT do: it does not touch ownership. Ownership - * is allocation, and allocation is released by $DALLOC or by the - * owner's death -- not by handing back a channel. That split is the - * oracle's, not a convenience: on the lab a process holding an open - * channel never became the device's owner in the first place. + * Give a channel back: unlink it from the device, drop the reference it + * held, and end any ownership that channel was carrying. */ static void device_release_channel(struct vms_channel *ch) { struct vms_device *dev = ch->dev; + pid_t pid = ch->owner_linux_pid; spin_lock(&dev->lock); list_del(&ch->devlink); if (dev->refcnt > 0) dev->refcnt--; + device_release_implicit_owner_locked(dev, pid); spin_unlock(&dev->lock); } /* - * Release everything a dying process held: its channels, and any - * device it had allocated. A device allocated by a process that no - * longer exists would be permanently unallocatable, which is not a - * state VMS has. + * Release everything a dying process held: its channels (which ends any + * ownership resting on them) and any device it had allocated. + * + * ORACLE: STOP CHANHOLD -- a detached process whose only claim on TTA0: + * was one assigned channel -- put the device back to Owner "" with a + * reference count of 0. A device left owned by a process that no longer + * exists is not a state VMS has. */ void vms_proc_release_channels(struct vms_proc *proc) { @@ -320,10 +383,19 @@ void vms_proc_release_channels(struct vms_proc *proc) * ORACLE-PINNED SEMANTICS (docs/oracle/vax73-terminal-device.md * section 7), because the obvious guesses are both wrong: * - * - Assigning a channel does NOT make the caller the device's - * owner. On the lab an open channel to NLA0: left "Owner process" - * empty and "Owner process ID" 00000000 for as long as it was - * held. Ownership is $ALLOC's job. + * - A channel to a NON-SHAREABLE device that nobody owns DOES make + * the caller its owner, without allocating it. TTA0: on the lab sat + * at Owner "" / reference count 0; a bare OPEN/WRITE (a channel and + * nothing else) moved it to Owner "SYSTEM" / Owner process ID + * 20400216 / reference count 1, with no "allocated" in the status + * clause -- and a DEALLOCATE at that moment was refused + * %SYSTEM-W-DEVNOTALLOC. This is why the console shows an owner on + * a system where nobody ever ran ALLOCATE. + * + * - A channel to a SHAREABLE device confers nothing. The identical + * DCL sequence on NLA0: ("shareable, mailbox device") left + * Owner "" / Owner process ID 00000000 with only the reference + * count moving. * * - $ASSIGN to a device another process owns SUCCEEDS. A detached * process on the lab assigned a channel to OPA0: -- the console, @@ -380,6 +452,16 @@ long vms_ioctl_assign(struct vms_proc *proc, unsigned long arg) spin_lock(&dev->lock); dev->refcnt++; list_add_tail(&ch->devlink, &dev->chanlist); + /* + * Implicit ownership. Note what is NOT here: no reference is added + * for it (TTA0: showed one channel, one reference, and an owner), + * and `allocated` stays clear. + */ + if (!dev->shareable && dev->owner_linux_pid == 0) { + dev->owner_pid = proc->vms_pid; + dev->owner_linux_pid = proc->linux_pid; + dev->owner_uic = caller_uic(); + } spin_unlock(&dev->lock); spin_unlock(&vms_device_list_lock); @@ -434,33 +516,39 @@ long vms_ioctl_dassgn(struct vms_proc *proc, unsigned long arg) /* * $ALLOC - allocate a device to this process. * - * This, not $ASSIGN, is what makes a process a device's owner. Every - * branch below is a case observed on the ~/vax OpenVMS VAX V7.3 lab - * (docs/oracle/vax73-terminal-device.md sections 7-9): + * There is exactly ONE refusal, and it is about OWNERSHIP, not about + * allocation and not about channels: a device somebody else owns + * cannot be allocated. Both observed cases on the ~/vax OpenVMS VAX + * V7.3 lab reduce to it (docs/oracle/vax73-terminal-device.md + * section 7): * - * allocated to another process -> %SYSTEM-W-DEVALLOC + * owner holds an allocation -> %SYSTEM-W-DEVALLOC * ALLOCATE OPA0: from a detached process while the interactive * job held the console. - * another process holds channels -> %SYSTEM-W-DEVALLOC - * ALLOCATE NLA0: with "Owner process" empty and a reference - * count of 2 -- foreign channels alone are enough. - * already allocated to us -> success, reference count unchanged + * owner holds only a channel -> %SYSTEM-W-DEVALLOC + * CHANHOLD, a detached process whose only claim on TTA0: was one + * assigned channel, showed as "Owner process CHANHOLD" with the + * status clause carrying no "allocated"; ALLOCATE TTA0: from the + * interactive job was refused all the same. + * already allocated to us -> success, reference count unchanged * ALLOCATE OPA0: twice in a row: 2 -> 3 -> 3. - * otherwise -> success, reference count + 1 + * ours by channel, or free -> success, reference count + 1 + * ALLOCATE OPA0: from the job that already owned it (by channel, + * not by allocation): reference count 2 -> 3, and the status + * clause gained the word "allocated". * - * The caller's UIC is taken from its Linux credentials. That mapping - * is NOT oracle-pinned and is noted as such in vms-d0b's findings; it - * will have to agree with whatever replaces the VMS_UIC_* environment - * facade (vms-2b8). + * WHAT IS NOT MODELLED (rule 10 -- do not invent a handler for a + * condition that was never measured): %SYSTEM-W-DEVASSIGN, "device has + * channels assigned" (2120). VMS clearly has this condition -- its own + * message facility printed the text -- but no probe ever provoked it, + * so OVMX does not return it anywhere. Filed rather than guessed. */ long vms_ioctl_alloc(struct vms_proc *proc, unsigned long arg) { struct vms_alloc_args args; struct vms_device *dev; - struct vms_channel *ch; char devnam[VMS_DEVNAM_SIZE]; uint32_t status; - int foreign = 0; memset(&args, 0, sizeof(args)); if (copy_from_user(&args, (void __user *)arg, sizeof(args))) @@ -482,29 +570,17 @@ long vms_ioctl_alloc(struct vms_proc *proc, unsigned long arg) } spin_lock(&dev->lock); - if (dev->allocated && dev->owner_linux_pid == proc->linux_pid) { - args.status = SS__NORMAL; /* already ours; idempotent */ - } else if (dev->allocated) { + if (dev->owner_linux_pid != 0 && dev->owner_linux_pid != proc->linux_pid) { args.status = SS__DEVALLOC; + } else if (dev->allocated) { + args.status = SS__NORMAL; /* already ours; idempotent */ } else { - list_for_each_entry(ch, &dev->chanlist, devlink) { - if (ch->owner_linux_pid != proc->linux_pid) { - foreign = 1; - break; - } - } - if (foreign) { - args.status = SS__DEVALLOC; - } else { - dev->allocated = 1; - dev->owner_pid = proc->vms_pid; - dev->owner_linux_pid = proc->linux_pid; - dev->owner_uic = - (((uint32_t)from_kgid(&init_user_ns, current_gid()) & 0xFFFFu) << 16) | - ((uint32_t)from_kuid(&init_user_ns, current_uid()) & 0xFFFFu); - dev->refcnt++; - args.status = SS__NORMAL; - } + dev->allocated = 1; + dev->owner_pid = proc->vms_pid; + dev->owner_linux_pid = proc->linux_pid; + dev->owner_uic = caller_uic(); + dev->refcnt++; + args.status = SS__NORMAL; } spin_unlock(&dev->lock); spin_unlock(&vms_device_list_lock); @@ -518,9 +594,17 @@ long vms_ioctl_alloc(struct vms_proc *proc, unsigned long arg) /* * $DALLOC - give an allocated device back. * - * Deallocating a device this process does not have allocated is - * %SYSTEM-W-DEVNOTALLOC on the lab (a second DEALLOCATE OPA0: right - * after the first). + * Deallocating a device this process does not have ALLOCATED is + * %SYSTEM-W-DEVNOTALLOC on the lab, and "does not have allocated" + * includes a device it owns by channel: DEALLOCATE TTA0: while holding + * only an open channel to it was refused %SYSTEM-W-DEVNOTALLOC with + * Owner "SYSTEM" left in place. A second DEALLOCATE OPA0: right after + * the first is refused the same way. + * + * What a successful $DALLOC does NOT do is make the device unowned: + * DEALLOCATE OPA0: took the reference count 3 -> 2 and dropped the word + * "allocated", and the job that still held channels stayed Owner + * "SYSTEM". Ownership then follows the implicit rule. */ long vms_ioctl_dalloc(struct vms_proc *proc, unsigned long arg) { diff --git a/src/kernel/vms_internal.h b/src/kernel/vms_internal.h index 12b7cfcdc..e18adacea 100644 --- a/src/kernel/vms_internal.h +++ b/src/kernel/vms_internal.h @@ -211,21 +211,41 @@ struct vms_device { uint32_t devtype; /* device type code; 0 = Unknown */ /* - * Ownership comes from ALLOCATION, not from assigning a channel. - * This is measured, not assumed: on the ~/vax OpenVMS VAX V7.3 lab - * a process that held an open channel to NLA0: left the device's - * "Owner process" empty and its "Owner process ID" 00000000, while - * ALLOCATE set both and added the word "allocated" to the - * SHOW DEVICE/FULL status clause - * (docs/oracle/vax73-terminal-device.md sections 7-9). + * shareable mirrors the word the oracle prints in SHOW DEVICE/FULL's + * status clause. It decides whether a channel confers ownership, so + * it is not decoration. MEASURED, ~/vax OpenVMS VAX V7.3, node VAX2 + * (docs/oracle/vax73-terminal-device.md section 7): + * "Device NLA0: ... record-oriented device, shareable, mailbox + * device." -> shareable + * "Terminal TTA0: ... is online, record-oriented device, carriage + * control." -> not shareable + */ + uint32_t shareable; /* 1 = "shareable" in the status clause */ + + /* + * OWNERSHIP AND ALLOCATION ARE TWO DIFFERENT THINGS, and both are + * measured (docs/oracle/vax73-terminal-device.md section 7): + * + * - A channel to a NON-shareable device that nobody owns makes the + * assigner the OWNER, with no allocation. TTA0: went from + * Owner "" / refcount 0 to Owner "SYSTEM" / refcount 1 on a bare + * OPEN/WRITE, and its status clause still said only "is online, + * record-oriented device, carriage control" -- no "allocated". + * - A channel to a SHAREABLE device confers nothing. The same DCL + * sequence on NLA0: left Owner "" with the reference count + * moving 2 -> 3 -> 2. + * - $ALLOC sets `allocated`, and it is the only thing that does. + * - Ownership without allocation ends when the owner returns its + * last channel (CLOSE -> Owner "", refcount 0) or dies + * (STOP CHANHOLD -> Owner "", refcount 0). An ALLOCATION outlives + * the channel until $DALLOC or the owner's death. * - * refcnt is the device's "Reference count": one per assigned - * channel plus one for an outstanding allocation -- also measured - * (NLA0: 2 -> 3 -> 2 across an OPEN/CLOSE; OPA0: 2 -> 3 on - * ALLOCATE and back to 2 on DEALLOCATE). + * refcnt is the device's "Reference count": one per assigned channel + * plus one for an outstanding allocation. Implicit ownership costs + * no reference (TTA0: one channel -> refcount 1, owned). */ - uint32_t allocated; /* 1 while allocated to owner_* */ - uint32_t owner_pid; /* VMS pid of the allocating process */ + uint32_t allocated; /* 1 while $ALLOC'd to owner_* */ + uint32_t owner_pid; /* VMS pid of the owner, 0 = unowned */ pid_t owner_linux_pid; uint32_t owner_uic; uint32_t refcnt; @@ -240,10 +260,9 @@ struct vms_device { /* * Every channel currently assigned to this device, by any process. - * The device has to know this to answer $ALLOC: the oracle refuses - * to allocate a device that another process merely has channels to - * (ALLOCATE NLA0: -> %SYSTEM-W-DEVALLOC with the owner field still - * empty and a reference count of 2). + * The device has to know this to decide when implicit ownership + * ends: it ends when the owner has no channel left, not when any + * channel is returned. */ struct list_head chanlist; /* of vms_channel.devlink */ diff --git a/src/kernel/vms_ioctl.h b/src/kernel/vms_ioctl.h index dccfe6952..9a695c196 100644 --- a/src/kernel/vms_ioctl.h +++ b/src/kernel/vms_ioctl.h @@ -315,12 +315,22 @@ struct vms_register_args { /* * One row of the executive device table, as handed to userspace. * - * owner_pid is 0 when the device is not allocated -- ownership comes - * from $ALLOC, never from $ASSIGN (measured on the oracle; see - * docs/oracle/vax73-terminal-device.md section 7). `allocated` is the - * flag behind the word "allocated" in SHOW DEVICE/FULL's status - * clause. refcnt is the "Reference count": one per assigned channel - * plus one for an outstanding allocation. + * owner_pid and `allocated` are TWO DIFFERENT THINGS, exactly as the + * oracle prints them as two different things (measured; see + * docs/oracle/vax73-terminal-device.md section 7): + * + * - owner_pid is "Owner process ID". A device can be owned with no + * allocation at all: on the lab a bare OPEN/WRITE to the + * non-shareable terminal TTA0: moved it from Owner "" to + * Owner "SYSTEM" / 20400216 with no "allocated" in its status + * clause, and the console OPA0: shows Owner "SYSTEM" on a system + * where nobody has run ALLOCATE. A channel to a SHAREABLE device + * (NLA0:) confers nothing. + * - `allocated` is the flag behind the word "allocated" in SHOW + * DEVICE/FULL's status clause, and only $ALLOC sets it. + * + * refcnt is the "Reference count": one per assigned channel plus one + * for an outstanding allocation. Ownership itself costs no reference. * * opcnt/errcnt are the * "Operations completed" and "Error count" SHOW DEVICE/FULL reports. @@ -336,7 +346,7 @@ struct vms_devinfo { char devnam[VMS_DEVNAM_SIZE]; /* physical name, e.g. "OPA0:" */ uint32_t devclass; /* DC$_ device class */ uint32_t devtype; /* device type code; 0 = Unknown */ - uint32_t owner_pid; /* VMS pid of the owner, 0 = not allocated */ + uint32_t owner_pid; /* VMS pid of the owner, 0 = unowned */ uint32_t owner_uic; /* (group << 16) | member */ uint32_t refcnt; /* channels assigned + allocation */ uint32_t errcnt; /* Error count */ @@ -350,13 +360,19 @@ struct vms_devinfo { /* * $ALLOC / $DALLOC: allocate a device to this process, and give it - * back. This is what makes a process the device's OWNER; $ASSIGN does - * not (oracle, docs/oracle/vax73-terminal-device.md section 7). + * back. Allocation is not the only route to ownership -- see + * struct vms_devinfo -- but it is the only thing that makes a device + * "allocated", and it holds the device after the last channel is gone. + * + * $ALLOC returns SS$_DEVALLOC when the device is OWNED by another + * process, whether that owner allocated it (ALLOCATE OPA0: from a + * detached process while the interactive job held the console) or + * merely assigned a channel to it (ALLOCATE TTA0: while the detached + * CHANHOLD process held one channel and no allocation). Both are + * measured; see docs/oracle/vax73-terminal-device.md section 7. * - * $ALLOC returns SS$_DEVALLOC when the device is allocated to another - * process, or when another process merely holds channels to it -- both - * observed on the lab. $DALLOC returns SS$_DEVNOTALLOC when this - * process does not have it allocated. + * $DALLOC returns SS$_DEVNOTALLOC when this process does not have the + * device ALLOCATED -- including when it owns the device by channel. */ struct vms_alloc_args { char devnam[VMS_DEVNAM_SIZE]; From fbed4a7e5cd503e1091dc3ba55c5b5185919e13d Mon Sep 17 00:00:00 2001 From: Chris Baron Date: Thu, 30 Jul 2026 10:39:47 -0400 Subject: [PATCH 6/8] vms-d0b: split process A into two phases so each ownership rule is proven 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 --- tests/qemu/test_kmod_devtab.c | 219 ++++++++++++++++++++++++++-------- 1 file changed, 172 insertions(+), 47 deletions(-) diff --git a/tests/qemu/test_kmod_devtab.c b/tests/qemu/test_kmod_devtab.c index 268ab7bad..76e1587df 100644 --- a/tests/qemu/test_kmod_devtab.c +++ b/tests/qemu/test_kmod_devtab.c @@ -20,9 +20,17 @@ * against a real /dev/vms, not a hand-rolled ioctl copy, so the client * that libvms will call is the client under test. * + * Process A runs in two phases so that the two ownership rules can be + * observed SEPARATELY -- if A always assigned and allocated in one + * breath, either rule alone would be enough to make every refusal come + * out right, and deleting one of them would leave the suite green. + * + * phase 1: A holds nothing but a CHANNEL. It is already the owner. + * phase 2: A allocates, on B's word. + * * Modes: - * (no args) process B (parent) - * --owner process A: assigns OPA0:, changes it, reports + * (no args) process B (parent) + * --owner process A */ #include @@ -59,7 +67,7 @@ static int pass = 0, fail = 0; else { printf(" FAIL: %s\n", msg); fail++; } \ } while (0) -/* What process A reports back over a pipe. */ +/* What process A reports back over a pipe, once per phase. */ struct owner_report { uint32_t assign_status; uint32_t setmode_status; @@ -68,6 +76,7 @@ struct owner_report { uint32_t chan; uint32_t owner_after_assign;/* owner the executive shows after $ASSIGN alone */ uint32_t refcnt_after_assign; + uint32_t alloc_after_assign;/* "allocated" flag after $ASSIGN alone */ uint32_t owner_pid; /* owner after $ALLOC */ uint32_t refcnt; /* reference count after $ALLOC */ uint32_t allocated; @@ -87,15 +96,16 @@ static int open_and_register(void) } /* - * process_a - assigns a channel to the console, allocates it (which is - * what makes it the owner -- $ASSIGN does not, per the oracle), and - * changes characteristics that it never tells anyone about except + * process_a - phase 1: take a channel to the console and stop there, so + * that B can see what a channel alone does. Phase 2, once B says so: + * allocate, and change characteristics nobody is told about except * through the executive. */ -static int process_a(int wfd) +static int process_a(int wfd, int rfd) { struct owner_report rep; struct vms_devinfo info; + char go; memset(&rep, 0, sizeof(rep)); @@ -108,15 +118,25 @@ static int process_a(int wfd) return 1; } + /* ---- phase 1: a channel and nothing else ---- */ rep.assign_status = vms_kif_assign(CONSOLE, &rep.chan); if (rep.assign_status == SS_NORMAL) { - /* After $ASSIGN alone: the oracle says we are NOT the owner. */ memset(&info, 0, sizeof(info)); if (vms_kif_getdvi_chan(rep.chan, &info) == SS_NORMAL) { rep.owner_after_assign = info.owner_pid; rep.refcnt_after_assign = info.refcnt; + rep.alloc_after_assign = info.allocated; } + } + if (write(wfd, &rep, sizeof(rep)) != (ssize_t)sizeof(rep)) + return 1; + + /* Wait for B, on B's word -- not on a timer. */ + if (read(rfd, &go, 1) != 1) + return 1; + /* ---- phase 2: allocate, and change the terminal ---- */ + if (rep.assign_status == SS_NORMAL) { rep.alloc_status = vms_kif_alloc(CONSOLE); /* Allocating a device we already have is a no-op on the lab, * including its reference count. */ @@ -150,15 +170,15 @@ static int process_a(int wfd) int main(int argc, char **argv) { - int pipefd[2]; + int pipefd[2], gofd[2]; pid_t child; struct owner_report rep; struct vms_devinfo info; uint32_t status, chan = 0, bogus_chan, index, scanned, saw_console; - char wfd_arg[16]; + char wfd_arg[16], rfd_arg[16]; - if (argc >= 3 && strcmp(argv[1], "--owner") == 0) - return process_a(atoi(argv[2])); + if (argc >= 4 && strcmp(argv[1], "--owner") == 0) + return process_a(atoi(argv[2]), atoi(argv[3])); printf("=== test_kmod_devtab: executive device table ===\n"); @@ -220,7 +240,7 @@ int main(int argc, char **argv) * takes ownership and changes its characteristics. This process * did none of that and must see all of it. * -------------------------------------------------------------- */ - if (pipe(pipefd) < 0) { + if (pipe(pipefd) < 0 || pipe(gofd) < 0) { printf(" FAIL: pipe()\n"); return 1; } @@ -233,14 +253,17 @@ int main(int argc, char **argv) if (child == 0) { close(pipefd[0]); + close(gofd[1]); /* Take our own channel rather than the inherited descriptor. */ vms_kif_close(); snprintf(wfd_arg, sizeof(wfd_arg), "%d", pipefd[1]); - execl(argv[0], argv[0], "--owner", wfd_arg, (char *)NULL); + snprintf(rfd_arg, sizeof(rfd_arg), "%d", gofd[0]); + execl(argv[0], argv[0], "--owner", wfd_arg, rfd_arg, (char *)NULL); _exit(73); } close(pipefd[1]); + close(gofd[0]); memset(&rep, 0, sizeof(rep)); if (read(pipefd[0], &rep, sizeof(rep)) != (ssize_t)sizeof(rep)) { printf(" FAIL: owner process never reported\n"); @@ -251,21 +274,90 @@ int main(int argc, char **argv) } CHECK(rep.assign_status == SS_NORMAL, "another process assigns a channel to OPA0:"); - CHECK(rep.setmode_status == SS_NORMAL, "owner sets terminal characteristics"); - /* ORACLE (VAX 7.3, NLA0: held open by one process): a channel does - * not confer ownership. The owner field stayed empty and the - * process ID stayed 00000000 for as long as the channel was held; - * only the reference count moved. */ - CHECK(rep.owner_after_assign == 0, - "$ASSIGN alone does not make the caller the device's owner"); + /* -------------------------------------------------------------- + * 3a. A HOLDS ONLY A CHANNEL. Nothing in the system is allocated. + * + * ORACLE (VAX 7.3, node VAX2, /tmp/clean-vax1-test/vax2.log): + * TTA0: -- a TERMINAL, whose SHOW DEVICE/FULL status clause + * carries no "shareable" -- went from + * "Owner process """ / "Reference count 0" (l.1121,1123) + * to + * "Owner process "SYSTEM"" / "Owner process ID 20400216" + * / "Reference count 1" (l.1136-1138) + * on a bare OPEN/WRITE (l.1127), with its status clause still + * reading "is online, record-oriented device, carriage control" + * -- no "allocated" (l.1132). A DEALLOCATE at that instant was + * refused "%SYSTEM-W-DEVNOTALLOC, device not allocated" (l.1143). + * + * The counter-case pins the criterion: the identical sequence on + * NLA0:, "shareable, mailbox device" (l.1176), left + * "Owner process """ (l.1179). Shareability is what decides, + * which is why the assertions below are about OPA0:, a terminal, + * and not about the null device. + * -------------------------------------------------------------- */ + CHECK(rep.owner_after_assign == (uint32_t)child, + "oracle: $ASSIGN to a non-shareable device nobody owns makes the caller its owner"); + CHECK(rep.alloc_after_assign == 0, + "oracle: ownership by channel is not an allocation"); CHECK(rep.refcnt_after_assign == 1, - "$ASSIGN alone adds one to the reference count"); + "oracle: ownership by channel costs one reference, not two"); + + memset(&info, 0, sizeof(info)); + status = vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(status == SS_NORMAL && info.owner_pid == (uint32_t)child, + "B sees the ownership A took with a channel alone (A writes, B reads)"); + CHECK(info.allocated == 0, "B sees that nothing is allocated"); + + /* + * THE ISOLATING CASE for the $ALLOC refusal, and the one the old + * foreign-channel rule was invented for. Measured directly: with + * the detached process CHANHOLD holding one channel to TTA0: and no + * allocation -- + * "Owner process "CHANHOLD"" (l.1005) + * "Reference count 1" (l.1007) + * $ ALLOCATE TTA0: (l.1009) + * %SYSTEM-W-DEVALLOC, device already allocated to another user + * (l.1010) + */ + status = vms_kif_alloc(CONSOLE); + CHECK(status == SS_DEVALLOC, + "oracle: $ALLOC is refused while another process owns the device by channel alone"); + memset(&info, 0, sizeof(info)); + (void)vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(info.owner_pid == (uint32_t)child && info.allocated == 0, + "the refused $ALLOC neither took ownership nor allocated anything"); + + /* And $DALLOC of a device owned by channel is DEVNOTALLOC (l.1143). */ + status = vms_kif_dalloc(CONSOLE); + CHECK(status == SS_DEVNOTALLOC, + "oracle: $DALLOC of a device nobody has allocated returns SS$_DEVNOTALLOC"); + + /* -------------------------------------------------------------- + * 3b. Now let A allocate. Synchronised on B's word, not on a timer. + * -------------------------------------------------------------- */ + if (write(gofd[1], "g", 1) != 1) { + printf(" FAIL: could not release the owner process\n"); + kill(child, SIGKILL); + waitpid(child, NULL, 0); + return 1; + } + memset(&rep, 0, sizeof(rep)); + if (read(pipefd[0], &rep, sizeof(rep)) != (ssize_t)sizeof(rep)) { + printf(" FAIL: owner process never reported phase 2\n"); + kill(child, SIGKILL); + waitpid(child, NULL, 0); + printf("=== test_kmod_devtab: %d passed, %d failed ===\n", pass, fail + 1); + return 1; + } - /* ORACLE: ALLOCATE is what sets Owner process / Owner process ID - * and adds the word "allocated" to SHOW DEVICE/FULL, and doing it - * twice changes nothing further (OPA0: 2 -> 3 -> 3). */ - CHECK(rep.alloc_status == SS_NORMAL, "$ALLOC of a free device succeeds"); + CHECK(rep.setmode_status == SS_NORMAL, "owner sets terminal characteristics"); + + /* ORACLE: ALLOCATE by the process that already owned OPA0: by + * channel added the word "allocated" and took the reference count + * 2 -> 3 (l.670-682); doing it twice changed nothing further + * (3 -> 3). */ + CHECK(rep.alloc_status == SS_NORMAL, "$ALLOC of a device we own by channel succeeds"); CHECK(rep.realloc_status == SS_NORMAL, "$ALLOC of a device we already have allocated succeeds"); CHECK(rep.owner_pid == (uint32_t)child, "executive records the allocating process as owner"); @@ -293,20 +385,30 @@ int main(int argc, char **argv) * VAX V7.3 lab, not chosen (docs/oracle/vax73-terminal-device.md * section 7): * - * a detached process, $ASSIGN OPA0: -> %SYSTEM-S-NORMAL + * a detached process, $ASSIGN OPA0: -> %SYSTEM-S-NORMAL (l.536) * the same process, ALLOCATE OPA0: -> %SYSTEM-W-DEVALLOC, - * device already allocated to another user + * device already allocated to another user (l.548) * * So a terminal owned by somebody else is assignable but not - * allocatable. Neither line below is OVMX's opinion. + * allocatable. The console stayed Owner "SYSTEM" / 20400216 + * across that foreign $ASSIGN (l.531-533 vs l.543-545), so the + * channel did not move ownership; note honestly that the + * reference count was back to 2 by the time SHOW DEVICE ran, so + * what that pins is "ownership did not transfer", not "a foreign + * channel was still open". * -------------------------------------------------------------- */ status = vms_kif_assign(CONSOLE, &chan); CHECK(status == SS_NORMAL && chan != 0, "oracle: $ASSIGN to a device another process owns returns SS$_NORMAL"); + memset(&info, 0, sizeof(info)); + status = vms_kif_getdvi_chan(chan, &info); + CHECK(status == SS_NORMAL && info.owner_pid == (uint32_t)child, + "oracle: $ASSIGN to an owned device does not transfer ownership"); + status = vms_kif_alloc(CONSOLE); CHECK(status == SS_DEVALLOC, - "oracle: $ALLOC of a device another process owns returns SS$_DEVALLOC"); + "oracle: $ALLOC of a device another process has allocated returns SS$_DEVALLOC"); memset(&info, 0, sizeof(info)); status = vms_kif_getdvi_chan(chan, &info); @@ -334,11 +436,23 @@ int main(int argc, char **argv) CHECK(status == SS_NOSUCHDEV, "allocating an absent device fails with SS$_NOSUCHDEV"); /* -------------------------------------------------------------- - * 5. The device outlives its owner. When A dies the executive - * takes the allocation back -- the device is the executive's, - * not A's. (A device left allocated to a process that no longer - * exists is not a state VMS has.) + * 5. The device outlives its owner. When A dies the executive takes + * back everything A held -- the device is the executive's, not + * A's. ORACLE: STOP CHANHOLD put TTA0: back to Owner "" with a + * reference count of 0 (l.1036-1038). + * + * B gives its own channel back FIRST, so that when A dies nobody + * else is holding the console. Whether a surviving channel-holder + * would inherit ownership was never measured, and this test does + * not assert an answer to it. * -------------------------------------------------------------- */ + status = vms_kif_dassgn(chan); + CHECK(status == SS_NORMAL, "channel deassigned"); + memset(&info, 0, sizeof(info)); + (void)vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(info.refcnt == 2 && info.owner_pid == (uint32_t)child, + "giving our channel back drops one reference and leaves the owner alone"); + kill(child, SIGKILL); waitpid(child, NULL, 0); @@ -347,29 +461,39 @@ int main(int argc, char **argv) CHECK(status == SS_NORMAL, "device still exists after its owner dies"); CHECK(info.owner_pid == 0, "dead process no longer owns the device"); CHECK(info.allocated == 0, "dead process's allocation was released"); - CHECK(info.refcnt == 1, "dead process's channel and allocation were both released"); + CHECK(info.refcnt == 0, "dead process's channel and allocation were both released"); CHECK(info.width == A_WIDTH && info.page == A_PAGE, "characteristics set by the dead process persist in the executive"); - /* Now that A is gone the device is free, so B can take it. */ + /* -------------------------------------------------------------- + * 6. The whole ownership life cycle in ONE process, in the order + * the oracle showed it on TTA0: and OPA0:. + * -------------------------------------------------------------- */ + status = vms_kif_assign(CONSOLE, &chan); + CHECK(status == SS_NORMAL, "console is assignable again once its owner is gone"); + memset(&info, 0, sizeof(info)); + (void)vms_kif_getdvi_devnam(CONSOLE, &info); + CHECK(info.owner_pid == (uint32_t)getpid() && info.allocated == 0 && info.refcnt == 1, + "oracle: a channel to the free console makes us its owner, unallocated (TTA0: l.1136-1138)"); + status = vms_kif_alloc(CONSOLE); - CHECK(status == SS_NORMAL, "device is allocatable again once its owner is gone"); + CHECK(status == SS_NORMAL, "device is allocatable once its owner is gone"); memset(&info, 0, sizeof(info)); (void)vms_kif_getdvi_devnam(CONSOLE, &info); - CHECK(info.owner_pid == (uint32_t)getpid() && info.allocated == 1, - "executive records the new owner"); + CHECK(info.owner_pid == (uint32_t)getpid() && info.allocated == 1 && info.refcnt == 2, + "oracle: allocating what we already own adds the allocation and one reference (OPA0: 2 -> 3, l.682)"); + status = vms_kif_dalloc(CONSOLE); - CHECK(status == SS_NORMAL, "$DALLOC gives the device back"); + CHECK(status == SS_NORMAL, "$DALLOC gives the allocation back"); memset(&info, 0, sizeof(info)); (void)vms_kif_getdvi_devnam(CONSOLE, &info); - CHECK(info.owner_pid == 0 && info.allocated == 0 && info.refcnt == 1, - "$DALLOC clears the owner and drops its reference"); + CHECK(info.allocated == 0 && info.refcnt == 1, + "oracle: $DALLOC drops the allocation and its reference (OPA0: 3 -> 2, l.695)"); + CHECK(info.owner_pid == (uint32_t)getpid(), + "oracle: $DALLOC does NOT unown a device we still hold a channel to (OPA0: still Owner \"SYSTEM\", l.693)"); - /* -------------------------------------------------------------- - * 6. Giving a channel back releases the reference. - * -------------------------------------------------------------- */ status = vms_kif_dassgn(chan); - CHECK(status == SS_NORMAL, "channel deassigned"); + CHECK(status == SS_NORMAL, "last channel deassigned"); status = vms_kif_dassgn(chan); CHECK(status == SS_IVCHAN, "deassigning a released channel reports SS$_IVCHAN"); @@ -377,7 +501,8 @@ int main(int argc, char **argv) status = vms_kif_getdvi_devnam(CONSOLE, &info); CHECK(status == SS_NORMAL && info.refcnt == 0, "reference count returns to zero"); - CHECK(info.owner_pid == 0, "device is unowned once the last channel is gone"); + CHECK(info.owner_pid == 0, + "oracle: returning the last channel unowns the device (TTA0: CLOSE -> Owner \"\", l.1165)"); vms_kif_close(); From ac10bbddf8babca46ec18439227b03e05b9af5df Mon Sep 17 00:00:00 2001 From: Chris Baron Date: Thu, 30 Jul 2026 10:44:30 -0400 Subject: [PATCH 7/8] vms-d0b: withdraw the foreign-channel claim in the oracle record, and 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 --- docs/oracle/vax73-terminal-device.md | 153 +++++++++++++++++++++++---- src/kernel/vms_devtab.c | 11 +- src/libvms/include/ssdef.h | 9 ++ 3 files changed, 150 insertions(+), 23 deletions(-) diff --git a/docs/oracle/vax73-terminal-device.md b/docs/oracle/vax73-terminal-device.md index 1817e81d8..f50d77246 100644 --- a/docs/oracle/vax73-terminal-device.md +++ b/docs/oracle/vax73-terminal-device.md @@ -141,9 +141,18 @@ $ SHOW DEVICE ZZA0: ## 7. Ownership, `$ASSIGN` and `$ALLOC` (captured 30-JUL-2026, node VAX2) -This section exists because the first cut of `src/kernel/vms_devtab.c` asserted an ownership rule as -VMS fact with nothing behind it ("the first channel to an unowned device makes its holder the -owner"). It was measured rather than argued. Method: a **second process** was created on VAX2 with +This section exists because `src/kernel/vms_devtab.c` twice asserted an ownership rule as VMS fact +with nothing behind it — first "the first channel to an unowned device makes its holder the owner", +then, after that was deleted, "another process merely holding channels refuses `$ALLOC`". The right +answer was not to argue about it a third time but to run the experiment; **the raw console log is +`/tmp/clean-vax1-test/vax2.log` on the lab host and every claim below cites its line numbers.** + +> **Note on the first claim.** It turns out to have been *right for the wrong reason* and was deleted +> on evidence that did not bear on it (a **shareable** device). Section 7.4 restores it, measured, on +> a **non-shareable** one. Sections 7.3 and 7.4 together are why: shareability is the criterion, and +> neither the original claim nor its deletion had tested that. + +Method: a **second process** was created on VAX2 with ``` $ RUN/DETACHED/INPUT=...DET.COM/OUTPUT=...DET.LOG/PROCESS_NAME=DEVPROBE SYS$SYSTEM:LOGINOUT.EXE @@ -189,9 +198,11 @@ assignable but not allocatable.** as `SYSTEM`. This capture therefore pins the *allocation* rule and says nothing about what an unprivileged process gets — device protection is a separate gate OVMX does not implement. -### 7.3 A channel does NOT confer ownership +### 7.3 A channel to a SHAREABLE device does not confer ownership -`NLA0:` before, during and after a channel was held by the observing process: +`NLA0:` — `SHOW DEVICE/FULL` calls it *"record-oriented device, **shareable**, mailbox device"* — +before, during and after a channel was held by the observing process (`vax2.log` l.562-592, and +again l.1172-1181): ``` $ SHOW DEVICE/FULL NLA0: Owner process "" Owner process ID 00000000 Reference count 2 @@ -204,18 +215,95 @@ $ SHOW DEVICE/FULL NLA0: Owner process "" Owner process ID 00000000 The owner fields never moved; only the reference count did. **Reference count is one per assigned channel.** -### 7.4 Foreign channels alone are enough to refuse `$ALLOC` +### 7.4 A channel to a NON-SHAREABLE device DOES confer ownership + +`TTA0:` is a terminal, and its status clause carries **no** `shareable`. The identical DCL sequence +gives the opposite answer (`vax2.log` l.1115-1167, verbatim): + +``` +$ SHOW DEVICE/FULL TTA0: + +Terminal TTA0:, device type unknown, is online, record-oriented device, carriage + control. + + Error count 0 Operations completed 0 + Owner process "" Owner UIC [SYSTEM] + Owner process ID 00000000 Dev Prot S:RWPL,O:RWPL,G,W + Reference count 0 Default buffer size 80 + +$ OPEN/WRITE X TTA0: +$ WRITE SYS$OUTPUT "R4-OPEN-ST=" + F$MESSAGE($STATUS) +R4-OPEN-ST=%SYSTEM-S-NORMAL, normal successful completion +$ SHOW DEVICE/FULL TTA0: + +Terminal TTA0:, device type unknown, is online, record-oriented device, carriage + control. + + Error count 0 Operations completed 0 + Owner process "SYSTEM" Owner UIC [SYSTEM] + Owner process ID 20400216 Dev Prot S:RWPL,O:RWPL,G,W + Reference count 1 Default buffer size 80 + +$ DEALLOCATE TTA0: +%SYSTEM-W-DEVNOTALLOC, device not allocated +$ CLOSE X +$ SHOW DEVICE/FULL TTA0: + ... Owner process "" Owner process ID 00000000 Reference count 0 +``` + +Four facts, all in that one block: + +1. **A channel alone makes the assigner the OWNER** of a non-shareable device that nobody owns. +2. It is **not an allocation**: the status clause still reads only *"is online, record-oriented + device, carriage control"*, and `DEALLOCATE` at that instant is refused `%SYSTEM-W-DEVNOTALLOC`. +3. Ownership like this **costs no extra reference** — one channel, reference count 1. +4. **Returning the last channel ends it**: `CLOSE` put the device back to `Owner ""` / count 0. + +This is why the console `OPA0:` shows `Owner process "SYSTEM"` on a system where nobody has ever run +`ALLOCATE` — the login job holds channels to it. -With `NLA0:` unowned but at reference count 2 (channels held by other processes): +### 7.5 `$ALLOC` is refused while another process owns the device by channel alone + +Independently reached from the other direction (`vax2.log` l.979-1038). `CHANHOLD` is a detached +process running a MACRO-32 image whose entire body is `$ASSIGN_S` to `TTA0:` followed by `$HIBER_S` +— one channel, no allocation: ``` -$ ALLOCATE NLA0: +$ SHOW DEVICE/FULL TTA0: + +Terminal TTA0:, device type unknown, is online, record-oriented device, carriage + control. + ... + Owner process "CHANHOLD" Owner UIC [SYSTEM] + Owner process ID 20400218 Dev Prot S:RWPL,O:RWPL,G,W + Reference count 1 Default buffer size 80 + +$ ALLOCATE TTA0: %SYSTEM-W-DEVALLOC, device already allocated to another user + +$ STOP CHANHOLD +$ SHOW DEVICE/FULL TTA0: + ... Owner process "" Owner process ID 00000000 Reference count 0 ``` -## 8. `ALLOCATE` sets the owner, adds a reference, and is idempotent +So there is **one** refusal, and it is about ownership: a device somebody else owns cannot be +allocated, whether that owner allocated it (7.2) or merely assigned a channel to it. Ownership also +dies with its owner. + +### 7.6 WITHDRAWN: "foreign channels alone are enough to refuse `$ALLOC`" -On the interactive job, which already owned `OPA0:` but had not allocated it: +An earlier revision of this document claimed, as section 7.4, that `ALLOCATE NLA0:` → +`%SYSTEM-W-DEVALLOC` (`vax2.log` l.622-623) proved that channels held by *other* processes refuse an +allocation. **That was an inference presented as a measurement, and it is withdrawn.** `NLA0:` was +at its idle baseline at the time — `Owner process ""`, reference count 2, the same 2 it had before +and after the observer's own `OPEN`/`CLOSE` — and nothing established that those two references +belonged to other processes. `NLA0:` is also `shareable`, which is a likelier reason `ALLOCATE` +refused it. **Why `ALLOCATE NLA0:` fails remains unexplained here and OVMX models nothing on it.** +The rule OVMX does implement is 7.5, which was measured directly on a non-shareable device. + +## 8. `ALLOCATE` sets `allocated`, adds a reference, and is idempotent + +On the interactive job, which already owned `OPA0:` — by channel, per 7.4 — but had not allocated it: ``` $ ALLOCATE OPA0: @@ -236,9 +324,19 @@ $ DEALLOCATE OPA0: %SYSTEM-W-DEVNOTALLOC, device not allocated ``` -Three things are pinned here: allocation adds the word **`allocated`** to the status clause; -allocation is worth **one reference**; and re-allocating a device you already have allocated -succeeds and changes nothing. +Four things are pinned here: allocation adds the word **`allocated`** to the status clause; +allocation is worth **one reference**; re-allocating a device you already have allocated succeeds and +changes nothing; and `DEALLOCATE` **does not unown the device** — the full row after the first +`DEALLOCATE` (l.689-695) is still + +``` + Owner process "SYSTEM" Owner UIC [SYSTEM] + Owner process ID 20400216 Dev Prot S:RWPL,O:RWPL,G,W + Reference count 2 Default buffer size 132 +``` + +because the job still holds channels to the console. Allocation and ownership come apart again here, +exactly as they did in 7.4. ## 9. Condition values, from VMS's own message facility @@ -262,9 +360,17 @@ Asked directly, by scanning `F$MESSAGE(n)` on the running V7.3 system: **This contradicts `src/libvms/include/ssdef.h` in several places.** The file's `SS$_NOMOREDEV` (2648) is right; its `SS$_DEVALLOC` (2316), `SS$_NOSUCHDEV` (2680), `SS$_IVCHAN` (602) and `SS$_IVDEVNAM` (608) are not. Only `SS$_DEVALLOC` was corrected as part of `vms-d0b` — it is the -constant this work introduces a use for, and it had no other consumer to break. The rest have a -blast radius across the kernel module, its client and its tests, and are tracked separately; do not -"fix" them without running the whole QEMU suite. +constant this work introduces a use for, and its two existing consumers +(`src/vmsdcl/dcl_cmd_misc.c`, `src/vmsfs/vmsfs_device.c`) name the symbol rather than the number, so +the correction does not break them. The rest have a blast radius across the kernel module, its +client and its tests, and are tracked separately; do not "fix" them without running the whole QEMU +suite. + +`2120 %SYSTEM-W-DEVASSIGN, device has channels assigned` is listed above because VMS's own message +facility printed it. **No probe ever provoked it**, so nothing in OVMX returns it: a condition known +only by its text is not a condition we can claim to reproduce (rule 10). Carried in `vms-d0b`'s +findings for filing: the probe that would settle it has to find the operation that raises +`DEVASSIGN`, not assume one. --- @@ -277,13 +383,16 @@ blast radius across the kernel module, its client and its tests, and are tracked | Unidentified type displays `Unknown` | Console registers with device type 0 = Unknown | | Characteristic **names** and their two-state form | `VMS_TTC_*` in `src/kernel/vms_ioctl.h`, one bit per oracle name | | Absent device → `%SYSTEM-W-NOSUCHDEV` | `SS$_NOSUCHDEV` from `$ASSIGN`/`$GETDVI` | -| `$ASSIGN` succeeds on a device another process owns (7.1) | `vms_ioctl_assign` returns `SS$_NORMAL` and does not touch ownership | -| Ownership comes from `$ALLOC`, never `$ASSIGN` (7.3, 8) | `vms_ioctl_alloc` sets `owner_pid`/`allocated`; `$ASSIGN` does not | -| `$ALLOC` of a device another process owns → `SS$_DEVALLOC` (7.2) | `vms_ioctl_alloc` returns `SS$_DEVALLOC` | -| `$ALLOC` refused while another process holds channels (7.4) | `vms_ioctl_alloc` walks `dev->chanlist` for a foreign holder | +| `$ASSIGN` succeeds on a device another process owns, and does not move ownership (7.1) | `vms_ioctl_assign` returns `SS$_NORMAL`; ownership is granted only when the device is unowned | +| A channel to a **shareable** device confers nothing (7.3) | `dev->shareable` guards the ownership grant in `vms_ioctl_assign` | +| A channel to a **non-shareable** device makes the assigner the owner, unallocated, at no extra reference (7.4) | `vms_ioctl_assign` sets `owner_*` when `!dev->shareable && owner_linux_pid == 0` | +| Returning the last channel unowns it; so does the owner's death (7.4, 7.5) | `device_release_implicit_owner_locked`, from `device_release_channel` and `vms_proc_release_channels` | +| `$ALLOC` of a device another process owns → `SS$_DEVALLOC`, whether they allocated it (7.2) or only assigned a channel (7.5) | one refusal in `vms_ioctl_alloc`, on `owner_linux_pid` | | Re-`$ALLOC` by the owner succeeds, no extra reference (8) | idempotent branch in `vms_ioctl_alloc` | -| `$DALLOC` of an unallocated device → `SS$_DEVNOTALLOC` (8) | `vms_ioctl_dalloc` | -| Reference count = channels + allocation (7.3, 8) | `refcnt` in the executive | +| `$ALLOC` by a process that owns the device by channel adds `allocated` and one reference (8) | `vms_ioctl_alloc` | +| `$DALLOC` of a device we have not ALLOCATED → `SS$_DEVNOTALLOC`, including one we own by channel (7.4, 8) | `vms_ioctl_dalloc` | +| `$DALLOC` drops the allocation and its reference but **not** ownership, while a channel is held (8) | `device_dealloc_locked` then the implicit rule | +| Reference count = channels + allocation; ownership itself is free (7.3, 7.4, 8) | `refcnt` in the executive | Deliberately **not** taken: diff --git a/src/kernel/vms_devtab.c b/src/kernel/vms_devtab.c index 1ef3405e4..476dee571 100644 --- a/src/kernel/vms_devtab.c +++ b/src/kernel/vms_devtab.c @@ -211,6 +211,13 @@ int vms_devtab_init(void) * "Terminal OPA0: ..." nor "Terminal TTA0: ..." carries the word * "shareable" in SHOW DEVICE/FULL's status clause, where NLA0: and * the MBAn: mailboxes do. + * + * DISCLOSED, not hidden: this is the ONLY device in the table, so + * the shareable = 1 side of the ownership rule -- measured on NLA0:, + * see section 7.3 -- has nothing to exercise it and no test asserts + * it. It is written because leaving it out would silently claim that + * every device confers ownership, which the oracle contradicts. The + * first shareable device added here owes the suite that assertion. */ console = vms_devtab_create(VMS_CONSOLE_DEVNAM, DC__TERM, VMS_DT_UNKNOWN, 0 /* shareable */, @@ -541,7 +548,9 @@ long vms_ioctl_dassgn(struct vms_proc *proc, unsigned long arg) * condition that was never measured): %SYSTEM-W-DEVASSIGN, "device has * channels assigned" (2120). VMS clearly has this condition -- its own * message facility printed the text -- but no probe ever provoked it, - * so OVMX does not return it anywhere. Filed rather than guessed. + * so OVMX does not return it anywhere and no code here is shaped around + * it. Carried in vms-d0b's findings; the probe that would settle it has + * to find the operation that raises DEVASSIGN, not assume one. */ long vms_ioctl_alloc(struct vms_proc *proc, unsigned long arg) { diff --git a/src/libvms/include/ssdef.h b/src/libvms/include/ssdef.h index a32012448..a4d6590cc 100644 --- a/src/libvms/include/ssdef.h +++ b/src/libvms/include/ssdef.h @@ -247,6 +247,15 @@ extern "C" { * SS$_NOSUCHDEV above); correcting the rest has a blast radius across * the kernel module and its tests and is tracked separately, not done * here. + * + * SS$_DEVALLOC already had two consumers when this value was + * corrected -- src/vmsdcl/dcl_cmd_misc.c and src/vmsfs/vmsfs_device.c. + * Both name the symbol rather than the number, so neither breaks. + * Noted because an earlier version of this comment said the constant + * had no other consumer, which was simply false. Separately, and NOT + * fixed here because it is out of this change's scope: + * vmsfs_device.c returns SS$_DEVALLOC for a FULL DEVICE TABLE, which + * is the wrong condition entirely -- carried in vms-d0b's findings. */ #define SS$_DEVALLOC 2112 /* Device already allocated to another user */ #define SS$_DEVNOTALLOC 2136 /* Device not allocated */ From c3f7875a6492ca7ad450b224324b10e17187229b Mon Sep 17 00:00:00 2001 From: Chris Baron Date: Thu, 30 Jul 2026 11:04:41 -0400 Subject: [PATCH 8/8] vms-d0b: freeze the device-table ioctl layouts the way vms-8019 froze 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. --- src/kernel/vms_ioctl.h | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/kernel/vms_ioctl.h b/src/kernel/vms_ioctl.h index 7e7df528c..35832b818 100644 --- a/src/kernel/vms_ioctl.h +++ b/src/kernel/vms_ioctl.h @@ -447,6 +447,54 @@ struct vms_setmode_args { #define VMS_IOCTL_ALLOC _IOWR(VMS_IOC_MAGIC, 0x55, struct vms_alloc_args) #define VMS_IOCTL_DALLOC _IOWR(VMS_IOC_MAGIC, 0x56, struct vms_alloc_args) +/* + * The kernel module and the userspace client compile these structures + * separately, from this one header, and then pass them across the + * /dev/vms boundary by raw address. If a field is ever reordered, + * widened or padded differently on one side, every ioctl above starts + * reading the wrong offsets -- silently, and only at runtime, and only + * for the fields past the change. Freeze the layouts here so that + * failure is a compile error on whichever side moved. + * + * The ioctl encodings are asserted for the same reason and one more: + * _IOWR folds sizeof(struct) into the request number, so a size change + * ALSO renumbers the request. The executive would then reject it with + * -ENOTTY rather than mis-decode it -- a different symptom, same root + * cause, and equally worth catching before it ships. + * + * These values are measured, not chosen: aarch64 and x86_64 agree, + * because every field is a fixed-width type. + */ +_Static_assert(sizeof(struct vms_devinfo) == 72, + "struct vms_devinfo changed size -- kernel and userspace would disagree on device attribute offsets"); +_Static_assert(sizeof(struct vms_assign_args) == 24, + "struct vms_assign_args changed size -- $ASSIGN would decode at the wrong offsets"); +_Static_assert(sizeof(struct vms_dassgn_args) == 8, + "struct vms_dassgn_args changed size -- $DASSGN would decode at the wrong offsets"); +_Static_assert(sizeof(struct vms_getdvi_args) == 88, + "struct vms_getdvi_args changed size -- $GETDVI would decode at the wrong offsets"); +_Static_assert(sizeof(struct vms_devscan_args) == 80, + "struct vms_devscan_args changed size -- $DEVICE_SCAN would decode at the wrong offsets"); +_Static_assert(sizeof(struct vms_setmode_args) == 40, + "struct vms_setmode_args changed size -- IO$_SETMODE would decode at the wrong offsets"); +_Static_assert(sizeof(struct vms_alloc_args) == 24, + "struct vms_alloc_args changed size -- $ALLOC/$DALLOC would decode at the wrong offsets"); + +_Static_assert(VMS_IOCTL_ASSIGN == 0xC0185650u, + "VMS_IOCTL_ASSIGN encodes differently here than on the reference build"); +_Static_assert(VMS_IOCTL_DASSGN == 0xC0085651u, + "VMS_IOCTL_DASSGN encodes differently here than on the reference build"); +_Static_assert(VMS_IOCTL_GETDVI == 0xC0585652u, + "VMS_IOCTL_GETDVI encodes differently here than on the reference build"); +_Static_assert(VMS_IOCTL_DEVSCAN == 0xC0505653u, + "VMS_IOCTL_DEVSCAN encodes differently here than on the reference build"); +_Static_assert(VMS_IOCTL_TTSETMODE == 0xC0285654u, + "VMS_IOCTL_TTSETMODE encodes differently here than on the reference build"); +_Static_assert(VMS_IOCTL_ALLOC == 0xC0185655u, + "VMS_IOCTL_ALLOC encodes differently here than on the reference build"); +_Static_assert(VMS_IOCTL_DALLOC == 0xC0185656u, + "VMS_IOCTL_DALLOC encodes differently here than on the reference build"); + /* ================================================================ * Process table (executive-resident PCB directory) *