Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions distro/rootfs/vms/SYS0/SYSCOMMON/SYSEXE/SYSUAF.DAT
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
# System User Authorization File
# Format: USERNAME|PASSWORD_HASH|UIC_GROUP|UIC_MEMBER|DEFAULT_DIR|FLAGS|PRIVILEGES
# Password hash is SHA256 hex. Empty = no password required.
SYSTEM||1|4|SYS$SYSDEVICE:[SYSMGR]||ALL
# SYSTEM/GUEST carry REAL hashes (vms-72c) -- the plaintext passwords
# tests/uat/vms_session_qemu.sh and this comment both name are MANAGER and
# GUEST respectively. Before this item every account here shipped an EMPTY
# hash, so sysuaf_authenticate() (src/libvms/rtl/sysuaf.c) accepted ANY
# password for ANY account -- measured directly against a real QEMU boot:
# 'SYSTEM' + a deliberately wrong password reached the DCL prompt. That is
# the exact veracity gap vms-72c's DONE CONDITION names ("a bad password is
# refused"), so it is closed here rather than only in the login code path.
# OPERATOR/DEFAULT/USER1/USER2 are UNCHANGED and remain passwordless --
# not exercised by any login test, and giving every shipped account a real
# password is a broader security hardening pass this item did not measure
# or scope; reported, not silently fixed.
SYSTEM|36a708df24b4751520ee64bba2d92167294acbb8f8fbfc3a120fb75323e9739b|1|4|SYS$SYSDEVICE:[SYSMGR]||ALL
OPERATOR||1|6|SYS$SYSDEVICE:[SYSMGR]||OPER,SYSPRV,TMPMBX,NETMBX
DEFAULT||200|200|SYS$SYSDEVICE:[USERS.DEFAULT]||TMPMBX,NETMBX
GUEST||200|201|SYS$SYSDEVICE:[USERS.GUEST]||TMPMBX
GUEST|16ceb2796ccd9d52d4f2a92134ef9ecfeb8f016150a82d36b299d09d5b9963f0|200|201|SYS$SYSDEVICE:[USERS.GUEST]||TMPMBX
USER1||200|202|SYS$SYSDEVICE:[USERS.USER1]||TMPMBX,NETMBX
USER2||200|203|SYS$SYSDEVICE:[USERS.USER2]||TMPMBX,NETMBX
51 changes: 44 additions & 7 deletions src/ovmx_init/ovmx_init.c
Original file line number Diff line number Diff line change
Expand Up @@ -1355,15 +1355,52 @@ int main(void)
(void)vms_kif_assign(OVMX_CONSOLE_DEVICE, &console_chan);
(void)vms_kif_setterm(console_chan);

/* Child: exec vms_login */
/* Child: exec vms_login (SYS$SYSTEM:LOGINOUT.EXE). */
execl(loginout_path, "vms_login", (char *)NULL);
/* If vms_login not found, exec vmsdcl directly */
execl(dcl_path, "vmsdcl", (char *)NULL);
/* Both failed — report why (show VMS specs in diagnostics) */
fprintf(stderr, "%%STARTUP-F-NOLOGIN, cannot exec %s: %s\n",

/*
* NO DCL FALLBACK (vms-72c). "exec vmsdcl directly" used to
* stand here if the LOGINOUT.EXE exec above failed -- an
* unauthenticated shell handed to whoever is at the console,
* reached by nothing more than a missing or unexecutable
* file. That is CLAUDE.md Rule 10's illegal third answer,
* named for exactly this shape in this item's own dispatch
* text: VMS has no state in which the console driver cannot
* run LOGINOUT and responds by starting an interactive
* session anyway with no username, no password and no
* SYSUAF check. It is also the same defect this item closes
* one line earlier for the empty-password-hash SYSUAF
* shipped by default (distro/rootfs/.../SYSUAF.DAT) --
* a second path to the identical outcome, "session reached
* with no real authentication", would have made that fix
* partial.
*
* MADE UNREACHABLE, NOT HANDLED, per Rule 10's other answer:
* LOGINOUT.EXE is a required system file, provisioned onto
* every system disk by provision_symlinks() (this file,
* "install once, boot forever" -- see is_system_installed())
* before the login loop below can ever run, so failing to
* exec it here is the same class of condition as vms.ko or
* /dev/vms being absent (executive_attach(), above) --
* OVMX's one runtime does not come up in that state. Unlike
* the executive gate, the response here is not to halt the
* whole boot: this is a per-login-attempt failure, not a
* per-system one, and the outer loop already retries with
* backoff (see "consecutive_failures" below) instead of
* surrendering the console -- NOT independently oracle-pinned
* here as "what VMS's console driver does on an image
* activation failure" (the ~/vax lab was unavailable for this
* item, mid-use for an unrelated experiment); it is the
* behavior this loop already had for every other login
* failure before this item touched it, kept unchanged. So the
* child reports why (OVMX facility, not a
* VMS one -- a Linux exec(2) failure has no VMS analogue,
* same reasoning as ovmx_exec_halt above) and exits, and the
* loop tries again; what it may not do is substitute an
* unauthenticated shell for the login it could not run.
*/
fprintf(stderr, "%%OVMX-E-NOLOGIN, cannot exec %s: %s\n",
VMS_LOGINOUT_PATH, strerror(errno));
fprintf(stderr, "%%STARTUP-F-NOLOGIN, cannot exec %s: %s\n",
VMS_DCL_PATH, strerror(errno));
_exit(1);
} else if (child > 0) {
/* Parent: wait for login session to end */
Expand Down
104 changes: 69 additions & 35 deletions src/vmsdcl/dcl_cmd_show.c
Original file line number Diff line number Diff line change
Expand Up @@ -945,10 +945,56 @@ static int cmd_show_process(struct dcl_command *cmd)
}

/*
* SHOW USERS - Show logged-in users from the terminal device table.
* SHOW USERS - Show logged-in users from the executive process table.
*
* Output matches OpenVMS format:
* Username Process Name PID Terminal
*
* ================================================================
* A READER OF THE EXECUTIVE PROCESS TABLE (vms-72c, Rule 11 corollary).
* ================================================================
*
* WHAT STOOD HERE was vms_term_list(), a reader of a file-based terminal
* allocation table (src/vmsdcl/dcl_terminal.c) whose only WRITER,
* vms_term_allocate(), vms-fb9 deleted for being the same self-declared-
* name shape as the rejected VMS_PRCNAM environment cheat (CLAUDE.md
* Rule 10, worked example 2). With no writer left, that table can never
* hold an entry, so vms_term_list()'s `count` was always 0 and the
* "no entries" branch always ran -- for every call, not as a fallback.
* That branch fabricated a SINGLE row out of the CALLING process's own
* DCL context: ctx->username, ctx->process_name, and getpid(). Measured
* on a real QEMU boot before this fix: an authenticated SYSTEM console
* session's SHOW USERS reported PID `00000049` -- a LINUX pid -- while
* the SAME session's SHOW PROCESS, one command earlier in the same
* transcript, reported the executive-assigned VMS pid `10000003` for
* the identical process. A second login could never appear, because
* nothing here ever looked past the caller.
*
* THE ROWS NOW COME FROM src/kernel/vms_proctab.c THROUGH
* vms_kif_procscan(), the same source cmd_show_system() and
* cmd_show_process() already read (this file), filtered to rows the
* executive has bound to a terminal (VMS_IOCTL_SETTERM, vms-d0b) --
* that is the same "is this job on a terminal" fact SHOW TERMINAL reads
* for the caller, applied here to every row instead of just the
* caller's own.
*
* A CROSS-GROUP SESSION MAY BE INVISIBLE HERE WITHOUT WORLD, and that is
* a KNOWN, DISCLOSED divergence, not this item's to close: proc_fill_info()
* (src/kernel/vms_proctab.c) redacts terminal along with the rest of a
* row's identity when vms_proc_may_read() says no (oracle-pinned,
* vax73-privileges.md Section 5 -- same-group needs no privilege,
* cross-group needs WORLD), so a redacted row's terminal reads "" and is
* skipped below exactly like an unbound one. cmd_show_system()'s own
* comment above records the identical divergence for its CPU column and
* why fixing it belongs to the redaction POLICY vms-8019 landed, not to
* a display reader -- the same reasoning applies here without repeating
* the whole argument.
*
* "Total number of users" is COUNTED BY WALKING THE SCAN, not carried in
* a separate hand-maintained variable (Method Requirement 4): the header
* line needs the count before the rows print, so the table is walked
* once to count and once to print rather than accumulated into a
* fixed-size buffer sized to a guessed maximum.
*/
static int cmd_show_users(struct dcl_command *cmd)
{
Expand All @@ -962,45 +1008,33 @@ static int cmd_show_users(struct dcl_command *cmd)
tm.tm_mday, vms_months[tm.tm_mon], 1900 + tm.tm_year,
tm.tm_hour, tm.tm_min, tm.tm_sec, (int)(ts.tv_nsec / 10000000));

struct terminal_device devs[100];
int count = 0;
vms_term_list(devs, 100, &count);
uint32_t index = 0;
struct vms_procinfo info;
int total = 0;

if (count == 0) {
/* No entries in device table — show at least the current user */
struct dcl_context *ctx = dcl_get_context();
char upper_name[64];
while (vms_kif_procscan(&index, &info) & 1) {
if (!info.redacted && info.terminal[0] != '\0')
total++;
}

printf(" Total number of users = %d, number of processes = %d\n\n",
total, total);
printf(" Username Process Name PID Terminal\n");

index = 0;
while (vms_kif_procscan(&index, &info) & 1) {
if (info.redacted || info.terminal[0] == '\0')
continue;

char upper_name[VMS_USERNAME_SIZE];
size_t i;
const char *src = ctx->username[0] ? ctx->username : "SYSTEM";
for (i = 0; i < sizeof(upper_name) - 1 && src[i]; i++)
upper_name[i] = (char)toupper((unsigned char)src[i]);
for (i = 0; i < sizeof(upper_name) - 1 && info.username[i]; i++)
upper_name[i] = (char)toupper((unsigned char)info.username[i]);
upper_name[i] = '\0';

printf(" Total number of users = 1, number of processes = 1\n\n");
printf(" Username Process Name PID Terminal\n");
printf(" %-12s %-16s %08X %s\n",
upper_name, ctx->process_name[0] ? ctx->process_name : upper_name,
(unsigned)getpid(),
/* No "_FTA0:" fallback (vms-fb9) -- see the note in
* cmd_show_process. An unknown terminal is reported as
* unknown, never as an invented device name. */
ctx->terminal.device_name);
} else {
printf(" Total number of users = %d, number of processes = %d\n\n",
count, count);
printf(" Username Process Name PID Terminal\n");

for (int j = 0; j < count; j++) {
char upper_name[64];
size_t i;
for (i = 0; i < sizeof(upper_name) - 1 && devs[j].owner_name[i]; i++)
upper_name[i] = (char)toupper((unsigned char)devs[j].owner_name[i]);
upper_name[i] = '\0';

printf(" %-12s %-16s %08X %s\n",
upper_name, upper_name,
(unsigned)devs[j].owner_pid, devs[j].name);
}
upper_name, info.prcnam, (unsigned)info.vms_pid,
info.terminal);
}

return SS$_NORMAL;
Expand Down
41 changes: 18 additions & 23 deletions src/vmsdcl/dcl_terminal.c
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,24 @@ static void term_table_save(FILE *fp, const struct terminal_device *devs, int co
* no user; under rule 10 a mechanism for a condition OVMX no longer has is
* deleted, not kept behind a lint. See src/vmsdcl/include/dcl/terminal.h.
*
* The reader/remover below stay because they still have callers; with the
* allocator gone the table can no longer gain an entry, so what they see
* is always empty. That is the honest state, not a bug to "fix" by putting
* the allocator back.
* The remover below stays because it still has a caller (dcl_main.c, at
* logout); with the allocator gone the table can no longer gain an entry,
* so what it removes is always nothing. That is the honest state, not a
* bug to "fix" by putting the allocator back.
*
* vms_term_list() -- THE READER -- WAS ALSO HERE AND IS ALSO DELETED
* (vms-72c). It had exactly the same "always sees an empty table" problem
* as the allocator's removal left behind, and its one caller
* (cmd_show_users() in src/vmsdcl/dcl_cmd_show.c) took that permanent
* emptiness as license to fabricate a single row about the CALLING
* process instead -- the same self-reporting shape Rule 10's worked
* examples reject, reproduced one layer up from where vms-fb9 already
* deleted it once. SHOW USERS now reads src/kernel/vms_proctab.c through
* vms_kif_procscan() directly, the same executive-resident source
* cmd_show_system() and cmd_show_process() already use, so this reader
* of a table that can never be written is deleted rather than kept
* behind a lint -- the same rule vms_term_allocate()'s deletion states
* above, applied to the half of the pair that survived it.
*/

void vms_term_deallocate(const char *device_name)
Expand Down Expand Up @@ -244,22 +258,3 @@ void vms_term_deallocate(const char *device_name)
fclose(fp);
}

void vms_term_list(struct terminal_device *out_devs, int max, int *count)
{
*count = 0;

FILE *fp = fopen(TERM_TABLE_PATH, "rb");
if (!fp) return;
flock(fileno(fp), LOCK_SH);

struct terminal_device devs[TERM_TABLE_MAX];
int total = term_table_load(fp, devs, TERM_TABLE_MAX);

flock(fileno(fp), LOCK_UN);
fclose(fp);

int n = (total < max) ? total : max;
for (int i = 0; i < n; i++)
out_devs[i] = devs[i];
*count = n;
}
15 changes: 10 additions & 5 deletions src/vmsdcl/include/dcl/terminal.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,17 @@ struct terminal_device {
* absent from the whole tree, definition included -- so re-adding it
* anywhere is what goes red, not re-adding a call in one named file.
*
* What remains below is the table's READ and REMOVE halves, which still
* have callers (SHOW USERS, DCL exit). With nothing left to add an entry
* they can only ever observe an empty table; converting their callers to
* the executive process table is vms-8019's scope, not this item's.
* What remained below used to be the table's READ and REMOVE halves,
* which had callers (SHOW USERS, DCL exit) that could only ever observe
* or clear an empty table. SHOW USERS's converted to the executive
* process table is vms-72c's, done: vms_term_list() -- the READ half --
* is deleted along with it, for the identical reason vms_term_allocate()
* above was: a mechanism whose one caller is gone and whose only
* possible answer was "nothing" is not kept behind a lint. Only the
* REMOVE half, vms_term_deallocate(), remains, because src/vmsdcl/
* dcl_main.c still calls it at logout; converting or deleting that
* caller is not this item's scope.
*/
void vms_term_deallocate(const char *device_name);
void vms_term_list(struct terminal_device *out_devs, int max, int *count);

#endif /* __DCL_TERMINAL_H */
73 changes: 46 additions & 27 deletions tests/dcl/test_show_users_terminal.sh
Original file line number Diff line number Diff line change
@@ -1,40 +1,59 @@
#!/bin/bash
# TEST: SHOW USERS reports process info without inventing a terminal name
# TEST: SHOW USERS fabricates no process row when it cannot read the executive
#
# COVERAGE CHANGED HERE AGAIN, DELIBERATELY (vms-72c) -- READ THIS BEFORE
# "FIXING" THE REGEX BACK TO REQUIRE A PID.
#
# vms-fb9 (the history section this file used to carry, kept below) got SHOW
# USERS halfway to honest: it stopped inventing a TERMINAL NAME, but it kept
# inventing a ROW -- cmd_show_users() read a file-based terminal-allocation
# table whose only WRITER vms-fb9 had already deleted, so the table was
# permanently empty and the "no entries" branch always ran, unconditionally.
# That branch fabricated a single row out of the CALLING process's own DCL
# context: ctx->username, getpid(), and ctx->terminal.device_name (empty,
# which is why the old EXPECT here only forbade a fabricated NAME, not a
# fabricated ROW -- the row's other fields were still invented, just less
# obviously so). This is why the old assertion (regex:[0-9A-F]{8}, i.e. "a
# PID must appear") is now WRONG rather than merely superseded: it was
# pinning the exact shape of the remaining half of the facade.
#
# SHOW USERS is now a READER of the executive's process table
# (src/kernel/vms_proctab.c, via vms_kif_procscan), the same source
# cmd_show_system() and cmd_show_process() already use (CLAUDE.md Rule 11:
# a user-visible VMS command reads an executive facility, it does not
# fabricate its own answer) -- and it lists a row only for a process the
# executive has actually bound to a terminal (VMS_IOCTL_SETTERM, vms-d0b).
#
# ctest runs on a host with no /dev/vms and never will have one: the only
# OVMX runtime is the kernel/QEMU path (Rule 9), exactly the reasoning
# test_show_system_no_fabrication.sh (this directory) already states for
# SHOW SYSTEM. So the positive proof -- SHOW USERS naming a REAL logged-in
# session by its REAL executive-assigned VMS PID and its REAL terminal --
# lives in tests/uat/vms_session_qemu.sh, against a real executive, a real
# console login and a real bound terminal. What CAN be proven here, and
# only here, is the thing that used to be wrong: with no executive to read
# (and so no process this invocation could possibly have bound a terminal
# for), SHOW USERS prints its banner, its header, and NO ROWS AT ALL.
# Restore the deleted per-process fallback and a row reappears with this
# invocation's own getpid() in it -- so this is a discriminating assertion
# about dcl_cmd_show.c's own code, not a decoration.
#
# EXPECT: contains:Username
# EXPECT: regex:[0-9A-F]{8}
# EXPECT: contains:Total number of users
# EXPECT_NOT: contains:Node
# EXPECT_NOT: contains:_FTA
# EXPECT_NOT: regex:_[A-Z]{2,3}[0-9]+:
# EXPECT_NOT: regex:[0-9A-F]{8}
#
# COVERAGE CHANGED HERE, DELIBERATELY, AND WHAT WAS LOST (vms-fb9).
#
# --- history (vms-fb9, superseded by the above) -----------------------
# This file used to read:
# # TEST: SHOW USERS displays VMS terminal device names and process info
# # EXPECT: regex:_[A-Z]{2,3}[0-9]+:
# and it passed because SHOW USERS printed
# ctx->terminal.device_name[0] ? ctx->terminal.device_name : "_FTA0:"
# -- i.e. on any system where DCL did not know its terminal (which is every
# system, since knowing it required the VMS_TERMINAL environment handoff this
# item deletes) it printed the literal "_FTA0:". The assertion was therefore
# satisfied by a hardcoded string, not by a terminal.
#
# So the old EXPECT was not coverage of a working feature; it was what kept
# the fallback alive. It is inverted here rather than deleted: the same
# pattern now has to be ABSENT, so reintroducing any invented device name
# under any prefix trips it.
#
# The capability is not abandoned. A VMS terminal name identifies a device in
# the executive's device table (src/kernel/vms_devtab.c), and which device a
# given job is on is executive process-table state OVMX does not have. Until
# then the column is empty, which is the honest report (CLAUDE.md rule 10).
#
# WHY EACH SURVIVING LINE IS NOT VACUOUS:
# contains:Username the report header printed, so the command ran and
# produced its table -- an empty terminal column is
# DCL answering, not DCL failing to run.
# regex:[0-9A-F]{8} the PID column, a real value, still printed. Paired
# with the header this pins that the ROW exists; the
# EXPECT_NOTs then pin what may not be in it.
# NOT contains:Node inherited unchanged and independent of the above.
# -- i.e. on any system where DCL did not know its terminal (which was every
# system, since knowing it required the VMS_TERMINAL environment handoff
# vms-fb9 deleted) it printed the literal "_FTA0:". The assertion was
# therefore satisfied by a hardcoded string, not by a terminal.
VMSDCL="${VMSDCL:-vmsdcl}"
echo 'SHOW USERS' | $VMSDCL 2>&1
Loading
Loading