From 40720923fd2e5fdbb79c865f28c328947bb2d471 Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 14 Sep 2026 14:04:12 +0000 Subject: [PATCH] vms-29b+vms-65b: faithful SSH->DCL handoff via $CREPRC(LOGINOUT) + executive netlogin-trust conveyance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retires the raw-execve DCL session shim (vms-16b) and lands the last layer of the booted-distro SSH login: an inbound `ssh SYSTEM@` reaches an authenticated DCL `$` through the SAME LOGINOUT/$CREPRC primitive console login and DECnet SET HOST use, per design docs/design-ssh-loginout-handoff.md (Option A: SSH authed in-protocol against SYSUAF/Purdy; LOGINOUT trusts it, no re-challenge). vms-65b -- the executive conveyance. Under PRC$M_LOGINOUT $CREPRC stamps no identity and $SETIDENT is self-targeted, so a daemon cannot reach into the LOGINOUT child it creates. New terminal-scoped ioctls convey the pre-authed user through the executive device table instead, modeled on the SETTERM channel-not-name precedent: - VMS_IOCTL_TERM_SETLOGIN (CAP_SYS_ADMIN/SETPRV-gated): a not-yet-dropped network daemon vouches the authed user onto the RTAn: it minted. - VMS_IOCTL_TERM_GETLOGIN (ordinary read): the $CREPRC(LOGINOUT) child bound to that terminal reads the note back for ITS OWN terminal; empty is the honest "no network pre-auth" -> prompt (fail-closed, INV-6). netlogin_user field on struct vms_device (both substrate copies); handlers in the shared kernel-core; wired through all registration sites (Linux + NetBSD dispatch, the ABI mirror + size/encoding asserts, libvmssys). The note is a NAME, never a credential -- LOGINOUT still builds the persona from the binary SYSUAF record and grants nothing beyond it. vms-65b -- LOGINOUT network mode (tools/vms_login.c). On startup, read the note off this session's own terminal (getjpi terminal -> GETLOGIN); if present, SYSUAF lookup -> loginout_network_permit (the landed fail-closed authorization floor) -> start_session() reused verbatim (identity stamp + UIC drop + staged-DCL activation, no password re-read). Absent falls through to the console prompt loop unchanged (console + DECnet re-challenge). vms-29b -- the sshd seam. The handoff runs in the still-privileged pre-drop window (__wrap_permanently_set_uid), the only point that still holds CAP_SYS_ADMIN to stamp the note and create a LOGINOUT that can establish SYSTEM to read the World-denied SYSUAF: mint vterm -> stamp note -> $CREPRC(LOGINOUT, vterm, PRC$M_INTER|PRC$M_LOGINOUT) -> pump the SSH channel <-> vterm until the session ends -> _exit. pw_shell now names the staged LOGINOUT.EXE (OpenSSH's shell-existence check targets the very image the session becomes; never execve'd -- the handoff _exits first). No fork/exec/ openpty in the daemon: ovmx_vterm_create and $CREPRC are the sole homes. Ground-source: test_kmod_devtab_terminal.c extended -- a real /dev/vms round trip of SETLOGIN/GETLOGIN, the honest-omission floor, the OPA0: guard, and a cross-process B-writes/A-reads proof (the SSH-stamps/LOGINOUT-reads path through the shared executive table, the vms-515 §7.5 anti-LARP bind). The live `ssh SYSTEM@ -> DCL $` cold-boot e2e (vms-237) remains lab-gated (vms-101); the ssh-boot-e2e transport rides #1128. CI-completeness (registration + test coherence for the retired mechanism): - libvmssys_shr.vec: append the two new kif universals (VMS-native LINK resolves vms_kif_terminal_getlogin for LOGINOUT.EXE). - vms_kif.h: OVMX-UNWIRED vms_kif_terminal_setlogin -- its only caller is the wrapped OpenSSH sshd, built outside the caller-census CMake graph (dlm_xnode precedent); the getlogin read half stays census-wired via LOGINOUT. - test_syssvc_ssh_server.c: re-scope the vms-9cc in-process capstone. The raw-execve-DCL mechanism it asserted is retired by vms-16b; a bare KE process cannot activate LOGINOUT.EXE (needs PID 1 /run/ovmx-boot staging), so it now proves transport + SYSUAF/Purdy auth (valid-user ACCEPTED vs unknown REFUSED, the non-vacuity pair) + fail-closed handoff-initiation. The end-to-end lands-in-DCL relocates to the booted ssh-boot-e2e (vms-843a, marker OVMX_DCL_LANDED_843a) -- a more faithful home, nothing unproven. - tests/qemu/Dockerfile: stage LOGINOUT.EXE into the KE initramfs (build vms_login + cp to /vms/.../SYSEXE/loginout.exe). pw_shell now names LOGINOUT.EXE, and OpenSSH's allowed_user() stat()s pw_shell at auth time, so test_syssvc_ssh_server's valid-user (SYSTEM) accept half was rejected before the SYSUAF read ("shell .../loginout.exe does not exist") in the bare KE env that stages DCL.EXE but not LOGINOUT. Test-env fix (the booted distro stages LOGINOUT via PID 1), the faithful analogue of the existing DCL.EXE staging. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_016PErgRQp9P1SegPxZyus3S --- src/kernel-core/vms_devtab.c | 126 ++++++++++++ src/kernel-netbsd/vms_acp_nb.h | 24 +++ src/kernel-netbsd/vms_internal.h | 10 + src/kernel-netbsd/vms_netbsd.c | 6 + src/kernel/vms_internal.h | 10 + src/kernel/vms_ioctl.h | 43 ++++ src/kernel/vms_module.c | 4 + src/libvmssys/vms_kif.c | 39 ++++ src/libvmssys/vms_kif.h | 22 +++ src/vmslink/libvmssys_shr.vec | 7 + src/vmsssh/sshd_auth.c | 49 ++++- src/vmsssh/sshd_auth.h | 9 + src/vmsssh/sshd_session.c | 264 ++++++++++++++++++------- src/vmsssh/sshd_session.h | 53 ++--- tests/qemu/Dockerfile | 26 ++- tests/qemu/test_kmod_devtab_terminal.c | 60 ++++++ tests/qemu/test_syssvc_ssh_server.c | 153 +++++++++----- tools/vms_login.c | 97 +++++++++ 18 files changed, 849 insertions(+), 153 deletions(-) diff --git a/src/kernel-core/vms_devtab.c b/src/kernel-core/vms_devtab.c index cc3d9295f..b25cde0fc 100644 --- a/src/kernel-core/vms_devtab.c +++ b/src/kernel-core/vms_devtab.c @@ -832,6 +832,132 @@ long vms_ioctl_term_resolve(struct vms_proc *proc, unsigned long arg) return 0; } +/* + * vms_ioctl_term_setlogin - stamp a network daemon's PRE-AUTHENTICATED user + * name onto an RTAn: (rd vms-65b). The conveyance channel for the SSH -> + * $CREPRC(LOGINOUT) handoff: the daemon has already authenticated the user in + * its own protocol against the SAME SYSUAF authority, and vouches that here so + * LOGINOUT does not re-challenge on a session it created for that terminal. + * + * PRIVILEGED: only a caller holding CAP_SYS_ADMIN/SETPRV (a not-yet-dropped + * network daemon -- the same authority that establishes a run-as identity) may + * vouch, exactly like vms_ioctl_establish_system. The note is a NAME, never a + * credential, and only a dynamic terminal (an RTAn:, never OPA0:) may carry + * one. LOGINOUT still builds the persona from the binary SYSUAF record and + * grants nothing beyond it. + */ +long vms_ioctl_term_setlogin(struct vms_proc *proc, unsigned long arg) +{ + struct vms_termlogin_args args; + struct vms_device *dev; + char devnam[VMS_DEVNAM_SIZE]; + uint32_t status; + + (void)proc; + + memset(&args, 0, sizeof(args)); + if (exec_copyin(&args, (const void *)arg, sizeof(args))) + return -EFAULT; + args.devnam[VMS_DEVNAM_SIZE - 1] = '\0'; + args.username[VMS_USERNAME_SIZE - 1] = '\0'; + + if (!exec_current_is_privileged()) { + args.status = SS__NOPRIV; + goto out; + } + + status = normalize_devnam(args.devnam, devnam, sizeof(devnam)); + if (status != SS__NORMAL) { + args.status = status; + goto out; + } + + exec_lock(&vms_device_list_lock); + dev = devtab_lookup_locked(devnam); + if (!dev) { + exec_unlock(&vms_device_list_lock); + args.status = SS__NOSUCHDEV; + goto out; + } + if (dev->devclass != DC__TERM || !dev->dynamic_term) { + /* Only a dynamically-minted RTAn: carries a network-login note; a + * static row (OPA0:) or a non-terminal is a category error, the same + * IVDEVNAM verdict resolve gives. */ + exec_unlock(&vms_device_list_lock); + args.status = SS__IVDEVNAM; + goto out; + } + exec_lock(&dev->lock); + memset(dev->netlogin_user, 0, sizeof(dev->netlogin_user)); + strscpy(dev->netlogin_user, args.username, sizeof(dev->netlogin_user)); + exec_unlock(&dev->lock); + exec_unlock(&vms_device_list_lock); + + args.status = SS__NORMAL; + +out: + if (exec_copyout((void *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} + +/* + * vms_ioctl_term_getlogin - read back the network-login note for a terminal + * (rd vms-65b). An ordinary read (like RESOLVE, unprivileged): the LOGINOUT + * child bound to an RTAn: asks for ITS OWN terminal's note. An empty note is + * the honest "no network pre-authentication" (SS$_NORMAL, empty username), on + * which LOGINOUT falls back to the interactive prompt (fail-closed, INV-6) -- + * NOT an error, because a terminal with no note is a perfectly ordinary + * console/DECnet terminal that authenticates its user itself. + */ +long vms_ioctl_term_getlogin(struct vms_proc *proc, unsigned long arg) +{ + struct vms_termlogin_args args; + struct vms_device *dev; + char devnam[VMS_DEVNAM_SIZE]; + uint32_t status; + + (void)proc; + + memset(&args, 0, sizeof(args)); + if (exec_copyin(&args, (const void *)arg, sizeof(args))) + return -EFAULT; + args.devnam[VMS_DEVNAM_SIZE - 1] = '\0'; + memset(args.username, 0, sizeof(args.username)); + + status = normalize_devnam(args.devnam, devnam, sizeof(devnam)); + if (status != SS__NORMAL) { + args.status = status; + goto out; + } + + exec_lock(&vms_device_list_lock); + dev = devtab_lookup_locked(devnam); + if (!dev) { + exec_unlock(&vms_device_list_lock); + args.status = SS__NOSUCHDEV; + goto out; + } + if (dev->devclass != DC__TERM || !dev->dynamic_term) { + exec_unlock(&vms_device_list_lock); + args.status = SS__IVDEVNAM; + goto out; + } + exec_lock(&dev->lock); + strscpy(args.username, dev->netlogin_user, sizeof(args.username)); + exec_unlock(&dev->lock); + exec_unlock(&vms_device_list_lock); + + /* Found: SS$_NORMAL whether or not a note is present -- the caller reads an + * empty username as "no network pre-auth" (the honest omission). */ + args.status = SS__NORMAL; + +out: + if (exec_copyout((void *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} + int vms_devtab_remove_terminal(const char *devnam) { struct vms_device *dev; diff --git a/src/kernel-netbsd/vms_acp_nb.h b/src/kernel-netbsd/vms_acp_nb.h index f306447d3..86a457f20 100644 --- a/src/kernel-netbsd/vms_acp_nb.h +++ b/src/kernel-netbsd/vms_acp_nb.h @@ -78,6 +78,13 @@ #define VMS_BACKING_SIZE 16 #endif +/* Username field width -- matches src/kernel/vms_ioctl.h's VMS_USERNAME_SIZE + * (32). Guarded like the widths above so this header composes with any other + * /dev/vms contract header that also defines it. */ +#ifndef VMS_USERNAME_SIZE +#define VMS_USERNAME_SIZE 32 +#endif + /* FIB$L_ACCTL access-control flag + the IO$_ACCESS name buffer size -- * byte-identical to src/kernel/vms_acp.h. */ #define VMS_ACP_ACCTL_WRITE 0x00000001u @@ -291,6 +298,15 @@ struct vms_terminal_args { uint32_t pad; }; +/* Mirror of struct vms_termlogin_args (src/kernel/vms_ioctl.h) -- the RTAn: + * network-login pre-authentication note (rd vms-65b). Byte-identical layout. */ +struct vms_termlogin_args { + char devnam[VMS_DEVNAM_SIZE]; /* the RTAn: terminal (in) */ + char username[VMS_USERNAME_SIZE]; /* SETLOGIN: in. GETLOGIN: out. */ + uint32_t status; /* return: SS$_ status */ + uint32_t pad; +}; + /* ================================================================ * Request numbers -- same NR band as src/kernel/vms_acp.h (0x68-0x6F); the * NetBSD _IOWR encoding of type/nr/size legitimately differs in VALUE from @@ -326,6 +342,8 @@ struct vms_terminal_args { #define VMS_IOCTL_TERM_CREATE _IOWR(VMS_ACP_IOC_MAGIC, 0x59, struct vms_terminal_args) #define VMS_IOCTL_TERM_DELETE _IOWR(VMS_ACP_IOC_MAGIC, 0x5a, struct vms_terminal_args) #define VMS_IOCTL_TERM_RESOLVE _IOWR(VMS_ACP_IOC_MAGIC, 0x5b, struct vms_terminal_args) +#define VMS_IOCTL_TERM_SETLOGIN _IOWR(VMS_ACP_IOC_MAGIC, 0x5c, struct vms_termlogin_args) +#define VMS_IOCTL_TERM_GETLOGIN _IOWR(VMS_ACP_IOC_MAGIC, 0x5d, struct vms_termlogin_args) /* * Freeze the shared layouts -- see src/kernel/vms_acp.h's identical asserts: @@ -363,11 +381,17 @@ _Static_assert(VMS_IOCTL_DISK_RESOLVE == 0xC0305657u, "VMS_IOCTL_DISK_RESOLVE encodes differently here than on the Linux reference build"); _Static_assert(sizeof(struct vms_terminal_args) == 40, "struct vms_terminal_args changed size -- RTAn: create/delete/resolve would decode at the wrong offsets"); +_Static_assert(sizeof(struct vms_termlogin_args) == 56, + "struct vms_termlogin_args changed size -- RTAn: netlogin note would decode at the wrong offsets"); _Static_assert(VMS_IOCTL_TERM_CREATE == 0xC0285659u, "VMS_IOCTL_TERM_CREATE encodes differently here than on the Linux reference build"); _Static_assert(VMS_IOCTL_TERM_DELETE == 0xC028565Au, "VMS_IOCTL_TERM_DELETE encodes differently here than on the Linux reference build"); _Static_assert(VMS_IOCTL_TERM_RESOLVE == 0xC028565Bu, "VMS_IOCTL_TERM_RESOLVE encodes differently here than on the Linux reference build"); +_Static_assert(VMS_IOCTL_TERM_SETLOGIN == 0xC038565Cu, + "VMS_IOCTL_TERM_SETLOGIN encodes differently here than on the Linux reference build"); +_Static_assert(VMS_IOCTL_TERM_GETLOGIN == 0xC038565Du, + "VMS_IOCTL_TERM_GETLOGIN encodes differently here than on the Linux reference build"); #endif /* _VMS_ACP_NB_H */ diff --git a/src/kernel-netbsd/vms_internal.h b/src/kernel-netbsd/vms_internal.h index 312eca515..9ae351c11 100644 --- a/src/kernel-netbsd/vms_internal.h +++ b/src/kernel-netbsd/vms_internal.h @@ -816,6 +816,14 @@ struct vms_device { */ uint32_t dynamic_term; + /* + * The SSH-pre-authenticated user name a network daemon vouched for this + * RTAn: (rd vms-65b), stamped by VMS_IOCTL_TERM_SETLOGIN and read back by + * the $CREPRC(LOGINOUT) child bound here (VMS_IOCTL_TERM_GETLOGIN). Empty + * unless a privileged daemon stamped it. Written/read under `lock`. + */ + char netlogin_user[VMS_USERNAME_SIZE]; + /* * Every channel currently assigned to this device, by any process: the * device has to know this to decide when IMPLICIT ownership ends (when the @@ -1073,6 +1081,8 @@ long vms_ioctl_disk_resolve(struct vms_proc *proc, unsigned long arg); long vms_ioctl_term_create(struct vms_proc *proc, unsigned long arg); long vms_ioctl_term_delete(struct vms_proc *proc, unsigned long arg); long vms_ioctl_term_resolve(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_term_setlogin(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_term_getlogin(struct vms_proc *proc, unsigned long arg); int vms_acp_dassgn(struct vms_proc *proc, uint32_t chan); void vms_acp_release_all(struct vms_proc *proc); /* diff --git a/src/kernel-netbsd/vms_netbsd.c b/src/kernel-netbsd/vms_netbsd.c index bfa2c0dc2..5351727b2 100644 --- a/src/kernel-netbsd/vms_netbsd.c +++ b/src/kernel-netbsd/vms_netbsd.c @@ -961,6 +961,8 @@ vms_ioctl(dev_t self __unused, u_long cmd, void *data, int flag __unused, case VMS_IOCTL_TERM_CREATE: case VMS_IOCTL_TERM_DELETE: case VMS_IOCTL_TERM_RESOLVE: + case VMS_IOCTL_TERM_SETLOGIN: + case VMS_IOCTL_TERM_GETLOGIN: uarg = data; proc = vms_proc_get(l->l_proc->p_pid); if (proc == NULL) @@ -991,6 +993,10 @@ vms_ioctl(dev_t self __unused, u_long cmd, void *data, int flag __unused, r = vms_ioctl_term_delete(proc, (unsigned long)uarg); break; case VMS_IOCTL_TERM_RESOLVE: r = vms_ioctl_term_resolve(proc, (unsigned long)uarg); break; + case VMS_IOCTL_TERM_SETLOGIN: + r = vms_ioctl_term_setlogin(proc, (unsigned long)uarg); break; + case VMS_IOCTL_TERM_GETLOGIN: + r = vms_ioctl_term_getlogin(proc, (unsigned long)uarg); break; default: return ENOTTY; /* unreachable */ } diff --git a/src/kernel/vms_internal.h b/src/kernel/vms_internal.h index 34f5f8953..16d93ae13 100644 --- a/src/kernel/vms_internal.h +++ b/src/kernel/vms_internal.h @@ -1042,6 +1042,14 @@ struct vms_device { */ uint32_t dynamic_term; + /* + * The SSH-pre-authenticated user name a network daemon vouched for this + * RTAn: (rd vms-65b), stamped by VMS_IOCTL_TERM_SETLOGIN and read back by + * the $CREPRC(LOGINOUT) child bound here (VMS_IOCTL_TERM_GETLOGIN). Empty + * unless a privileged daemon stamped it. Written/read under `lock`. + */ + char netlogin_user[VMS_USERNAME_SIZE]; + /* * Every channel currently assigned to this device, by any process. * The device has to know this to decide when implicit ownership @@ -1327,6 +1335,8 @@ long vms_ioctl_disk_resolve(struct vms_proc *proc, unsigned long arg); long vms_ioctl_term_create(struct vms_proc *proc, unsigned long arg); long vms_ioctl_term_delete(struct vms_proc *proc, unsigned long arg); long vms_ioctl_term_resolve(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_term_setlogin(struct vms_proc *proc, unsigned long arg); +long vms_ioctl_term_getlogin(struct vms_proc *proc, unsigned long arg); /* * Internal (non-ioctl) twin of disk_resolve for an in-executive caller: the * Files-11 ODS-2 ACP $MOUNT (vms-127) resolves a canonical disk-unit name to its diff --git a/src/kernel/vms_ioctl.h b/src/kernel/vms_ioctl.h index f22587351..631b152ff 100644 --- a/src/kernel/vms_ioctl.h +++ b/src/kernel/vms_ioctl.h @@ -2192,6 +2192,49 @@ _Static_assert(VMS_IOCTL_TERM_RESOLVE == 0xC028565Bu, */ #define VMS_USERNAME_SIZE 32 +/* + * NETWORK-LOGIN PRE-AUTHENTICATION NOTE on a dynamic terminal (rd vms-65b). + * + * The conveyance channel that lets an inbound network daemon which has ALREADY + * authenticated a user in its own protocol (SSH: cryptographic/Purdy password + * against the same SYSUAF authority) hand that user to a $CREPRC(LOGINOUT, + * RTAn:, PRC$M_LOGINOUT) session WITHOUT LOGINOUT re-challenging (Option A, + * design docs/design-ssh-loginout-handoff.md). Under PRC$M_LOGINOUT the creator + * stamps NO identity (sys_process.c) and $SETIDENT is self-targeted, so the + * daemon cannot reach into the LOGINOUT child; instead it stamps the + * pre-authenticated user name onto the RTAn: DEVICE record it minted, and the + * LOGINOUT child -- bound to that same terminal by creprc_bind_terminal -- + * reads it back for ITS OWN terminal. + * + * SETLOGIN is CAP_SYS_ADMIN/SETPRV-gated (exec_current_is_privileged): only a + * trusted, not-yet-dropped network daemon may vouch a pre-authentication -- the + * same authority INETD/sshd hold to establish a run-as identity. The note is a + * user NAME, never a credential: LOGINOUT still builds the persona from the + * binary SYSUAF record and grants nothing beyond it. GETLOGIN is an ordinary + * read (like RESOLVE): absent/empty is the honest "no network pre-auth", on + * which LOGINOUT falls back to the interactive prompt (fail-closed, INV-6). + * + * A DEDICATED arg struct (not a widened vms_terminal_args) so the RTAn: + * create/delete/resolve request numbers and their frozen size are untouched. + * Placed after VMS_USERNAME_SIZE because the username field uses it. + */ +struct vms_termlogin_args { + char devnam[VMS_DEVNAM_SIZE]; /* the RTAn: terminal (in) */ + char username[VMS_USERNAME_SIZE]; /* SETLOGIN: in. GETLOGIN: out. */ + uint32_t status; /* return: SS$_ status */ + uint32_t pad; +}; + +#define VMS_IOCTL_TERM_SETLOGIN _IOWR(VMS_IOC_MAGIC, 0x5c, struct vms_termlogin_args) +#define VMS_IOCTL_TERM_GETLOGIN _IOWR(VMS_IOC_MAGIC, 0x5d, struct vms_termlogin_args) + +_Static_assert(sizeof(struct vms_termlogin_args) == 56, + "struct vms_termlogin_args changed size -- RTAn: netlogin note would decode at the wrong offsets"); +_Static_assert(VMS_IOCTL_TERM_SETLOGIN == 0xC038565Cu, + "VMS_IOCTL_TERM_SETLOGIN encodes differently here than on the reference build"); +_Static_assert(VMS_IOCTL_TERM_GETLOGIN == 0xC038565Du, + "VMS_IOCTL_TERM_GETLOGIN encodes differently here than on the reference build"); + /* * Invoking CLI command-line bound (vms-f60d). OVMX DESIGN CHOICE * (CLAUDE.md Rule 8): 256 bytes holds the classic 255-character DCL diff --git a/src/kernel/vms_module.c b/src/kernel/vms_module.c index 05abcf350..220346a76 100644 --- a/src/kernel/vms_module.c +++ b/src/kernel/vms_module.c @@ -2209,6 +2209,10 @@ static long vms_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg return vms_ioctl_term_delete(proc, arg); case VMS_IOCTL_TERM_RESOLVE: return vms_ioctl_term_resolve(proc, arg); + case VMS_IOCTL_TERM_SETLOGIN: + return vms_ioctl_term_setlogin(proc, arg); + case VMS_IOCTL_TERM_GETLOGIN: + return vms_ioctl_term_getlogin(proc, arg); case VMS_IOCTL_SETTERM: return vms_ioctl_setterm(proc, arg); diff --git a/src/libvmssys/vms_kif.c b/src/libvmssys/vms_kif.c index ba620dd5f..498395716 100644 --- a/src/libvmssys/vms_kif.c +++ b/src/libvmssys/vms_kif.c @@ -1497,6 +1497,45 @@ uint32_t vms_kif_terminal_resolve(const char *devnam, char *backing, return args.status; } +uint32_t vms_kif_terminal_setlogin(const char *devnam, const char *username) +{ + struct vms_termlogin_args args; + + if (!devnam || !username) + 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'; + vms_strncpy(args.username, username, VMS_USERNAME_SIZE - 1); + args.username[VMS_USERNAME_SIZE - 1] = '\0'; + + KIF_CALL(VMS_IOCTL_TERM_SETLOGIN, &args); + return args.status; +} + +uint32_t vms_kif_terminal_getlogin(const char *devnam, char *username, + uint32_t username_size) +{ + struct vms_termlogin_args args; + + if (!devnam || !username || username_size == 0) + 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'; + + KIF_CALL(VMS_IOCTL_TERM_GETLOGIN, &args); + + username[0] = '\0'; + if (args.status & 1) { + vms_strncpy(username, args.username, username_size - 1); + username[username_size - 1] = '\0'; + } + return args.status; +} + uint32_t vms_kif_getvol(const char *devnam, struct vms_getvol_args *out) { struct vms_getvol_args args; diff --git a/src/libvmssys/vms_kif.h b/src/libvmssys/vms_kif.h index 8dd2a93b8..dd2cd87fa 100644 --- a/src/libvmssys/vms_kif.h +++ b/src/libvmssys/vms_kif.h @@ -665,6 +665,28 @@ uint32_t vms_kif_terminal_delete(const char *devnam); uint32_t vms_kif_terminal_resolve(const char *devnam, char *backing, uint32_t backing_size); +/* + * Stamp / read the SSH-pre-authenticated network-login note on a dynamic + * terminal (rd vms-65b). setlogin is CAP_SYS_ADMIN/SETPRV-gated (a network + * daemon vouching a pre-authentication before it drops privilege); getlogin is + * an ordinary read. On getlogin, an EMPTY *username with SS$_NORMAL is the + * honest "no network pre-auth" -- the caller (LOGINOUT) then prompts. + * + * OVMX-UNWIRED: vms_kif_terminal_setlogin (vms-65b) -- the STAMP is emitted by + * the wrapped OpenSSH sshd (src/vmsssh/sshd_session.c, ovmx_sshd_pre_drop_pw), + * which is not a CMake product target: it is built as a separate musl-static + * binary by third-party/openssh/build-ssh-harness.sh and reached from OpenSSH's + * own main() through --wrap=permanently_set_uid, so the caller census (which + * follows the CMake product graph) cannot see the call -- the same footing as + * vms_kif_dlm_xnode above, whose caller (scsd) is likewise a separately-built + * daemon. The READ half (vms_kif_terminal_getlogin) IS census-wired: LOGINOUT + * (tools/vms_login.c) is a CMake product target and calls it. Retire this line + * if the wrapped-sshd sources ever join the census's product graph. + */ +uint32_t vms_kif_terminal_setlogin(const char *devnam, const char *username); +uint32_t vms_kif_terminal_getlogin(const char *devnam, char *username, + uint32_t username_size); + /* 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. diff --git a/src/vmslink/libvmssys_shr.vec b/src/vmslink/libvmssys_shr.vec index ce7dfc7f9..fd2f83428 100644 --- a/src/vmslink/libvmssys_shr.vec +++ b/src/vmslink/libvmssys_shr.vec @@ -258,3 +258,10 @@ vms_kif_setterm=PROCEDURE vms_kif_terminal_create=PROCEDURE vms_kif_terminal_delete=PROCEDURE vms_kif_terminal_resolve=PROCEDURE + +# vms-65b: the RTAn: network-login pre-auth note (VMS_IOCTL_TERM_SETLOGIN/ +# GETLOGIN). The SSH session handoff stamps it and LOGINOUT.EXE (native-linked) +# reads it, so both cross the shareable boundary and must be universals here. +# APPENDED per the append-only contract -- never shifts an existing index. +vms_kif_terminal_setlogin=PROCEDURE +vms_kif_terminal_getlogin=PROCEDURE diff --git a/src/vmsssh/sshd_auth.c b/src/vmsssh/sshd_auth.c index ebfe876cf..c7ae65a67 100644 --- a/src/vmsssh/sshd_auth.c +++ b/src/vmsssh/sshd_auth.c @@ -10,17 +10,46 @@ */ #include +#include +#include #include #include "sshd_auth.h" #include "sysuaf.h" #include "str_util.h" -#include "ovmx_layout.h" +#include "ovmx_layout.h" /* VMS_LOGINOUT_PATH, ovmx_boot_stage_exec_path */ +#include "vmsfs/filespec.h" /* vmsfs_to_linux_path */ /* The DCL image, as a Linux path OpenSSH's execve() can run (mirrors - * vmssshd.c's DCL_SHELL_PATH). */ + * vmssshd.c's DCL_SHELL_PATH). Retained for ovmx_sshd_dcl_* below; the login + * SHELL is now LOGINOUT.EXE (the session-establish primitive), not DCL. */ #define OVMX_SSHD_DCL_PATH VMS_SYSTEM_DIR "/DCL.EXE" +/* + * Resolve SYS$SYSTEM:LOGINOUT.EXE to the Linux path that activates it -- the + * VMS filespec translation, then the boot-staging bridge when a staged copy is + * present -- the same resolution JOB_CONTROL and the DECnet CTERM host use + * (dnet_cterm_host.c cterm_host_loginout_path). Shared by the SSH session + * handoff (sshd_session.c: the $CREPRC image) and the getpwnam adapter below + * (pw_shell, so OpenSSH's shell-existence check targets the very image the + * session becomes). Returns 1 and fills *out on success, 0 on failure. + */ +int ovmx_sshd_loginout_path(char *out, size_t outsz) +{ + char staged[512]; + + if (out == NULL || outsz == 0) + return 0; + if (vmsfs_to_linux_path(VMS_LOGINOUT_PATH, out, outsz) != 1) + snprintf(out, outsz, "%s", VMS_LOGINOUT_PATH); + if (out[0] == '\0') + return 0; + if (ovmx_boot_stage_exec_path(out, staged, sizeof(staged)) && + access(staged, X_OK) == 0) + snprintf(out, outsz, "%s", staged); + return 1; +} + /* ---- the login decision (unit-tested, no SYSUAF file needed) ------------- */ int ovmx_sshd_check_login(const struct sysuaf_record *rec_opaque, @@ -103,7 +132,21 @@ int ovmx_sshd_fill_passwd(const char *user, struct passwd *pw, strncpy(b->passwd, "x", sizeof(b->passwd) - 1); /* unused: CUSTOM auth */ strncpy(b->gecos, rec.username, sizeof(b->gecos) - 1); strncpy(b->dir, "/", sizeof(b->dir) - 1); - strncpy(b->shell, OVMX_SSHD_DCL_PATH, sizeof(b->shell) - 1); + /* The login SHELL is LOGINOUT.EXE -- the session-establish image the SSH + * handoff $CREPRCs (sshd_session.c), NOT DCL. OpenSSH stats pw_shell for its + * shell-existence check, so it must name a path that exists on the VFS; the + * staged LOGINOUT.EXE is that path AND the image the session actually + * becomes. It is never execve'd here (the handoff runs in the pre-drop and + * _exits), so it is only the existence-check target -- but it is the honest + * one, not a decoy shell. On resolution failure keep the DCL path (no worse + * than before): the handoff itself then fails-closed. */ + { + char shellpath[512]; + if (ovmx_sshd_loginout_path(shellpath, sizeof(shellpath))) + strncpy(b->shell, shellpath, sizeof(b->shell) - 1); + else + strncpy(b->shell, OVMX_SSHD_DCL_PATH, sizeof(b->shell) - 1); + } memset(pw, 0, sizeof(*pw)); pw->pw_name = b->name; diff --git a/src/vmsssh/sshd_auth.h b/src/vmsssh/sshd_auth.h index b873f145c..e70245f7c 100644 --- a/src/vmsssh/sshd_auth.h +++ b/src/vmsssh/sshd_auth.h @@ -93,6 +93,15 @@ int ovmx_sshd_fill_passwd(const char *user, struct passwd *pw, /* The DCL image path used for pw_shell / the session exec (Linux path). */ const char *ovmx_sshd_dcl_image_path(void); +/* + * Resolve SYS$SYSTEM:LOGINOUT.EXE to the Linux path that activates it (the VMS + * filespec translation, then the boot-staging bridge when a staged copy is + * present). Returns 1 and fills *out on success, 0 on failure. Shared by the + * SSH session handoff (sshd_session.c: the $CREPRC image) and the getpwnam + * adapter (pw_shell). See sshd_auth.c. + */ +int ovmx_sshd_loginout_path(char *out, size_t outsz); + /* 1 if `path` names the DCL image (basename DCL.EXE) -- the session-shim test * for "OpenSSH is about to exec the login shell, which is DCL". */ int ovmx_sshd_is_dcl_path(const char *path); diff --git a/src/vmsssh/sshd_session.c b/src/vmsssh/sshd_session.c index 75f28a1a4..74dd8289d 100644 --- a/src/vmsssh/sshd_session.c +++ b/src/vmsssh/sshd_session.c @@ -1,101 +1,229 @@ /* - * sshd_session.c - LOGINOUT pre-drop setup for the wrapped OpenSSH sshd session - * seam (rd vms-0cd, RUNG-3 step 3c). See sshd_session.h. + * sshd_session.c - the SSH SESSION HANDOFF for the wrapped OpenVMS OpenSSH sshd + * seam (rd vms-843a / vms-29b / vms-65b). See sshd_session.h. * - * This is the SSH mirror of the pre-execl block in tools/vms_login.c (LOGINOUT) - * and the child of the old libssh vmssshd.c -- the SAME calls, same order, - * transplanted behind the OpenSSH permanently_set_uid seam so OpenSSH stays - * unmodified. The credential drop ITSELF is OpenSSH's own permanently_set_uid - * (from the SYSUAF UIC that sshd_auth.c put in the struct passwd), so cred_drop.c - * is not linked here -- OpenSSH performs the ordered, permanent, fail-closed - * drop natively. + * This is the FAITHFUL SSH -> DCL session establishment (design + * docs/design-ssh-loginout-handoff.md, Option A). It replaces the retired + * SSH C-reimpl session shim (the raw __real_execve of /vms/.../DCL.EXE, which + * ENOENTs on a booted distro where DCL.EXE is ODS-2/ACP-only) with the ONE + * session-establish primitive the console login and DECnet SET HOST already + * use: + * + * ovmx_vterm_create() # executive mints RTAn: + * -> VMS_IOCTL_TERM_SETLOGIN (vms-65b conveyance) # vouch the pre-authed user + * -> $CREPRC(LOGINOUT.EXE, RTAn:, PRC$M_INTER|PRC$M_LOGINOUT) + * -> pump the SSH channel <-> the vterm until the session ends + * + * SSH already authenticated the user IN-PROTOCOL against the SAME SYSUAF/Purdy + * authority (sshd_auth.c, proven vms-9cc), so LOGINOUT does NOT re-challenge: + * we stamp the pre-authenticated user name onto the RTAn: this handoff mints, + * and LOGINOUT -- created on that same terminal -- reads it back (network-login + * mode, tools/vms_login.c) and skips the prompt. The stamp is authorized here + * because this runs BEFORE OpenSSH's credential drop, while sshd is still root + * (VMS_IOCTL_TERM_SETLOGIN is CAP_SYS_ADMIN/SETPRV-gated). The note is a NAME, + * never a credential: LOGINOUT builds the persona from the binary SYSUAF record + * and grants nothing beyond it. + * + * WHY HERE (the pre-drop window, __wrap_permanently_set_uid) AND NOT __wrap_ + * execve: the created LOGINOUT child establishes SYSTEM to read the World-denied + * SYS$SYSTEM:SYSUAF.DAT, which needs the creator to still hold CAP_SYS_ADMIN -- + * true only before OpenSSH's permanently_set_uid drop. __wrap_execve runs AFTER + * the drop, too late to create a privileged LOGINOUT. + * + * NOTHING HERE forks, execs, or opens a pty: ovmx_vterm_create is the ONE home + * for the pty (below the VMS layer) and $CREPRC is the ONE home for process + * creation, exactly as the DECnet CTERM host uses them + * (tests/integration/test_creprc_session_primitive.sh gates that). This file + * only mints, stamps, creates, and relays bytes. */ +#define _GNU_SOURCE + #include #include -#include #include -#include +#include +#include +#include #include +#include #include "sshd_session.h" +#include "sshd_auth.h" /* ovmx_sshd_loginout_path (the LOGINOUT image) */ #include "sysuaf.h" -#include "ssh_ident.h" -#include "ovmx_accounting.h" -#include "ovmx_banner.h" -#include "vms_kif.h" +#include "vms_kif.h" /* vms_kif_terminal_setlogin (vms-65b) */ +#include "ovmx_vterm.h" /* ovmx_vterm_create/_delete */ +#include "ovmx_status.h" +#include "ssdef.h" +#include "starlet.h" /* sys$creprc */ +#include "descrip.h" +#include "prcdef.h" /* PRC$M_INTER, PRC$M_LOGINOUT */ + +/* Write the whole buffer or fail. Returns 0 on success, -1 on a hard error. */ +static int session_write_all(int fd, const char *buf, size_t len) +{ + size_t done = 0; + while (done < len) { + ssize_t n = write(fd, buf + done, len - done); + if (n > 0) { + done += (size_t)n; + continue; + } + if (n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) + continue; + return -1; + } + return 0; +} /* - * Production identity syscall table: the real vms_kif_setident behind the - * injectable seam (ssh_ident.h). Defined here (NOT in vmssshd.c, which the - * OpenSSH sshd does not link) so ovmx_ssh_establish_identity resolves in the - * wrapped sshd; the unit test (tests/vmsssh/test_ssh_ident.c) injects its own - * table and never references this symbol. + * Relay the SSH channel (this process's stdin/stdout, which OpenSSH's do_child + * has already pointed at the session channel -- pty slave for interactive, the + * materialized BGn: socket otherwise) to and from the vterm master, until the + * session ends. Then _exit -- this process IS the session relay and never + * returns to OpenSSH. + * + * client -> server : stdin -> master_fd (keystrokes / piped DCL commands) + * server -> client : master_fd -> stdout (DCL output, the login banner) + * + * The session ends when the vterm master EOFs (the DCL session logged out and + * LOGINOUT exited -> the slave closed), or the SSH client goes away. Client + * stdin EOF alone does NOT end it: a non-interactive `ssh host < maxfd) + maxfd = STDIN_FILENO; + } + + r = select(maxfd + 1, &rfds, NULL, NULL, NULL); + if (r < 0) { + if (errno == EINTR) + continue; + return; + } + + if (stdin_open && FD_ISSET(STDIN_FILENO, &rfds)) { + char buf[4096]; + ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); + if (n > 0) { + if (session_write_all(master_fd, buf, (size_t)n) != 0) + return; + } else if (n == 0) { + stdin_open = 0; /* client done sending; keep draining output */ + } else if (errno != EINTR && errno != EAGAIN) { + stdin_open = 0; + } + } + + if (FD_ISSET(master_fd, &rfds)) { + char buf[4096]; + ssize_t n = read(master_fd, buf, sizeof(buf)); + if (n > 0) { + if (session_write_all(STDOUT_FILENO, buf, (size_t)n) != 0) + return; + } else if (n == 0) { + return; /* DCL session ended: the slave closed */ + } else if (errno == EIO) { + return; /* pty peer gone: session over */ + } else if (errno != EINTR && errno != EAGAIN && + errno != EWOULDBLOCK) { + return; + } + } + } +} void ovmx_sshd_pre_drop_pw(const struct passwd *pw) { - static const char *const months[] = { - "JAN", "FEB", "MAR", "APR", "MAY", "JUN", - "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" - }; sysuaf_record_t rec; - uint32_t uic, ist = 0; - uint64_t privs; - time_t last_login = 0; + char devnam[VMS_DEVNAM_SIZE]; + char loginout_path[512]; + int master_fd = -1; + uint32_t st, pid = 0; if (pw == NULL || pw->pw_name == NULL) return; - /* Re-read the binary SYSUAF for the privilege mask + exact UIC. If this is - * not a SYSUAF account (the privsep 'sshd' user also routes through the - * permanently_set_uid wrap), do nothing -- let the real drop proceed. */ + /* SYSUAF account? A non-SYSUAF name (the privsep 'sshd' user, which also + * routes through the permanently_set_uid wrap) is NOT a login session: do + * nothing and let OpenSSH's real drop proceed. */ memset(&rec, 0, sizeof(rec)); if (sysuaf_lookup(pw->pw_name, &rec) != 0) return; - uic = (rec.uic_group << 16) | rec.uic_member; - privs = sysuaf_record_privileges(&rec); /* the $UAFDEF quadword mask */ - - /* - * ESTABLISH THE AUTHENTICATED IDENTITY IN THE EXECUTIVE, FAIL CLOSED - * (vms-6ae / INV-6, audit-vms-040 §3.7). Must precede the drop: setident - * needs the SETPRV the root registration granted. On refusal -- including - * /dev/vms absent (an even status) -- DENY the session: print - * %OVMX-F-NOIDENT and _exit(1), constructing no identity the executive - * refused. Mirrors tools/vms_login.c exactly. The diagnostic goes to the - * session's stdout (the PTY slave / channel), so the client sees it. - */ - if (ovmx_ssh_establish_identity(rec.username, uic, privs, - &ovmx_ident_real_syscalls, &ist) != 0) { - printf("%%OVMX-F-NOIDENT, the executive refused the authenticated " - "identity (status %u)\n", (unsigned)ist); + /* ---- from here the process NEVER returns to OpenSSH: it either becomes + * the session relay (success) or _exits fail-closed. A SYSUAF login does + * not fall through to OpenSSH's own credential drop + shell exec. ---- */ + + /* 1. Mint the virtual terminal for this SSH channel. The name comes BACK + * from the executive; this process does not choose it. */ + st = ovmx_vterm_create(devnam, sizeof(devnam), &master_fd); + if (!(st & 1)) { + printf("%%OVMX-F-NOTERM, could not create a terminal for the SSH " + "session (status %u)\n", (unsigned)st); fflush(stdout); _exit(1); } - /* SYS$WELCOME banner + last-interactive-login line (real, from the same - * accounting store the console login uses). */ - printf("\n"); - ovmx_banner_welcome(stdout); - if (ovmx_accounting_get_lastlogin(rec.username, &last_login) == 0 - && last_login > 0) { - struct tm *tm = localtime(&last_login); - if (tm) - printf("\n Last interactive login on %02d-%s-%04d " - "%02d:%02d:%02d\n\n", - tm->tm_mday, months[tm->tm_mon], tm->tm_year + 1900, - tm->tm_hour, tm->tm_min, tm->tm_sec); - else - printf("\n Last login time could not be determined.\n\n"); - } else { - printf("\n No previous interactive login recorded.\n\n"); + /* 2. Vouch the SSH-pre-authenticated user onto that RTAn: (vms-65b). We are + * still root here (pre-drop), so VMS_IOCTL_TERM_SETLOGIN is authorized. + * rec.username is the upcased SYSUAF key. */ + st = vms_kif_terminal_setlogin(devnam, rec.username); + if (!(st & 1)) { + printf("%%OVMX-F-NOTRUST, could not convey the authenticated identity " + "to LOGINOUT (status %u)\n", (unsigned)st); + fflush(stdout); + (void)ovmx_vterm_delete(devnam, master_fd); + _exit(1); + } + + /* 3. Create the session: LOGINOUT.EXE bound to the vterm, PRC$M_LOGINOUT so + * it establishes SYSTEM, reads SYSUAF, and re-personas to the vouched + * user. uic=0/prvadr=NULL: the creator stamps no identity (the note is + * the only thing conveyed). */ + if (!ovmx_sshd_loginout_path(loginout_path, sizeof(loginout_path))) { + (void)ovmx_vterm_delete(devnam, master_fd); + _exit(1); + } + { + struct dsc$descriptor_s img_d = dsc$init(loginout_path); + struct dsc$descriptor_s term_d = dsc$init(devnam); + + st = sys$creprc(&pid, &img_d, &term_d, &term_d, &term_d, + NULL, NULL, NULL, 0, 0, 0, + PRC$M_INTER | PRC$M_LOGINOUT); + if (!(st & 1)) { + /* No session, and nothing pretends otherwise (INV-6): there is no + * fallback that admits the peer anyway. */ + printf("%%OVMX-F-NOSESSION, could not create the LOGINOUT session " + "(status %u)\n", (unsigned)st); + fflush(stdout); + (void)ovmx_vterm_delete(devnam, master_fd); + _exit(1); + } } - /* Record this login (after showing the last, before the drop+DCL exec). */ - ovmx_accounting_record_login(rec.username); - fflush(stdout); + /* 4. Relay bytes until the session ends, then release the RTAn: this + * handoff minted (the creator owns it, as the DECnet CTERM host does) and + * _exit. The relay process held the vterm master; closing it here + the + * executive delete withdraws the dynamic unit rather than leaking it. */ + ovmx_sshd_pump(master_fd); + (void)ovmx_vterm_delete(devnam, master_fd); + _exit(0); } diff --git a/src/vmsssh/sshd_session.h b/src/vmsssh/sshd_session.h index 509689fc2..7ea1a4dee 100644 --- a/src/vmsssh/sshd_session.h +++ b/src/vmsssh/sshd_session.h @@ -1,43 +1,48 @@ /* - * sshd_session.h - the LOGINOUT pre-drop setup for the wrapped OpenVMS OpenSSH - * sshd session seam (rd vms-0cd, RUNG-3 step 3c, design §B). See sshd_session.c. + * sshd_session.h - the SSH session handoff for the wrapped OpenVMS OpenSSH sshd + * seam (rd vms-843a / vms-29b / vms-65b, design docs/design-ssh-loginout- + * handoff.md). See sshd_session.c. * * OpenSSH's do_child() drops to the login user (do_setusercontext -> * permanently_set_uid) and then execve()s the login shell. OVMX interposes at - * both points via linker --wrap (no OpenSSH source edit): + * permanently_set_uid via linker --wrap (no OpenSSH source edit) and, for a + * SYSUAF login, HANDS THE SESSION OFF to $CREPRC(LOGINOUT) instead of letting + * OpenSSH drop-and-exec a shell: * - * __wrap_permanently_set_uid -> ovmx_sshd_pre_drop_pw(pw) [still root] - * establish the executive identity (fail-closed), banner, accounting; - * __real_permanently_set_uid [OpenSSH's own - * ordered, permanent, fail-closed setgid-before-setuid drop to the UIC]; - * __wrap_execve -> rewrite the DCL exec into LOGINOUT->DCL argv (sshd_auth.c). + * __wrap_permanently_set_uid -> ovmx_sshd_pre_drop_pw(pw) [still root] + * SYSUAF login -> mint a vterm, vouch the pre-authenticated user onto it + * (VMS_IOCTL_TERM_SETLOGIN, vms-65b), $CREPRC(LOGINOUT, + * vterm, PRC$M_INTER|PRC$M_LOGINOUT), pump, _exit. + * NEVER returns -- OpenSSH's own drop + execve do not run. + * privsep 'sshd' -> no-op; return so __real_permanently_set_uid drops. * - * The pre-drop half MUST run as root (setident needs SETPRV from the root - * registration; accounting writes the protected store) -- exactly the ordering - * tools/vms_login.c and vmssshd.c use. This header stays free of the OVMX SYSUAF - * headers so the OpenSSH adapter can include it cleanly. + * This runs while still root ON PURPOSE: the created LOGINOUT establishes SYSTEM + * to read the World-denied SYS$SYSTEM:SYSUAF.DAT, which needs the creator to + * still hold CAP_SYS_ADMIN -- true only before OpenSSH's drop, and the same + * reason VMS_IOCTL_TERM_SETLOGIN (which vouches the pre-auth) is gated on it. + * + * This unifies console login (JOB_CONTROL -> LOGINOUT), DECnet SET HOST and SSH + * on the ONE $CREPRC(LOGINOUT) primitive, retiring the SSH C-reimpl session + * shim's raw-execve DCL activation (vms-16b). This header stays free of the OVMX + * SYSUAF headers so the OpenSSH adapter can include it cleanly. */ #ifndef OVMX_VMSSSH_SSHD_SESSION_H #define OVMX_VMSSSH_SSHD_SESSION_H +#include + struct passwd; /* */ /* - * Run the LOGINOUT pre-drop sequence for the login described by *pw, while the + * Hand off the login described by *pw to a $CREPRC(LOGINOUT) session, while the * process is still privileged (called from __wrap_permanently_set_uid before - * the real drop): - * - * 1. re-read the binary SYSUAF for pw->pw_name (privileges + exact UIC); - * 2. establish the authenticated identity in the executive - * (ovmx_ssh_establish_identity -> vms_kif_setident) -- FAIL CLOSED: on - * refusal (or /dev/vms absent) print %OVMX-F-NOIDENT and _exit(1), so NO - * session is ever handed an identity the executive denied (INV-6, the - * audit-§3.7 decision, mirrored from tools/vms_login.c); - * 3. print the SYS$WELCOME banner + last-interactive-login line; - * 4. record this login in the accounting store. + * the real drop). For a SYSUAF account this NEVER returns: on success it becomes + * the byte relay between the SSH channel and the LOGINOUT/DCL session and + * _exit()s when the session ends; on any failure to create the session it + * _exit(1)s fail-closed (INV-6 -- no session is ever admitted without one). * - * If pw->pw_name is NOT a SYSUAF account (e.g. the privsep 'sshd' user whose + * If pw->pw_name is NOT a SYSUAF account (the privsep 'sshd' user, whose * permanently_set_uid also routes through the wrap), this is a NO-OP and the * real drop proceeds unchanged. */ diff --git a/tests/qemu/Dockerfile b/tests/qemu/Dockerfile index 20a46ce5e..9db746cf0 100644 --- a/tests/qemu/Dockerfile +++ b/tests/qemu/Dockerfile @@ -242,12 +242,13 @@ COPY . /src/repo/ # (still a glob), not about which product image a suite needs. # BUILD_TOOLS=ON (vms-4c2): tools/CMakeLists.txt is only CONFIGURED here, not # fully built -- the --target list below still names exactly the binaries -# this image needs, so flipping this on adds one requested target -# (vms_authorize) without pulling vms_login/vms_mail/vms_monitor/etc. into -# the build. OVMX_STATIC + BUILD_TOOLS=ON is not new ground: distro/ +# this image needs, so flipping this on adds only the requested targets +# (vms_authorize; and vms_login -> LOGINOUT.EXE, the wrapped sshd's pw_shell +# subject for test_syssvc_ssh_server) without pulling vms_mail/vms_monitor/etc. +# into the build. OVMX_STATIC + BUILD_TOOLS=ON is not new ground: distro/ # Dockerfile.bootable already builds the WHOLE tools/ tree this way for the -# bootable distro image, so vms_authorize compiling static+musl here is -# precedented, not a first attempt. +# bootable distro image, so vms_authorize/vms_login compiling static+musl here +# is precedented, not a first attempt. RUN cd /src/repo && \ cmake -B build-static \ -DCMAKE_C_COMPILER=musl-gcc \ @@ -255,7 +256,7 @@ RUN cd /src/repo && \ -DOVMX_STATIC=ON \ -DBUILD_TESTS=ON \ -DBUILD_TOOLS=ON \ - && cmake --build build-static --target qemu_syssvc_tests vmsdcl vms_authorize vms_initialize mmk_native vmslibrarian vmslink vms_tcpip_daytime --parallel $(nproc) 2>&1 && \ + && cmake --build build-static --target qemu_syssvc_tests vmsdcl vms_login vms_authorize vms_initialize mmk_native vmslibrarian vmslink vms_tcpip_daytime --parallel $(nproc) 2>&1 && \ echo "--- test_syssvc_* binaries (public sys\$ API, static musl) ---" && \ ls -la build-static/bin/test_syssvc_* && \ echo "--- PIPESTAGE.EXE (the stage image test_syssvc_spawn_pipeline drives, vms-e9a B2) ---" && \ @@ -679,6 +680,19 @@ RUN mkdir -p /initramfs/bin /initramfs/lib/modules /initramfs/dev \ chmod +x /initramfs/vms/SYS0/SYSCOMMON/SYSEXE/MMK.EXE && \ cp /initramfs/bin/DCL.EXE /initramfs/vms/SYS0/SYSCOMMON/SYSEXE/DCL.EXE && \ chmod +x /initramfs/vms/SYS0/SYSCOMMON/SYSEXE/DCL.EXE && \ + # LOGINOUT.EXE at SYS$SYSTEM (vms-29b) -- the session image the wrapped + # OpenSSH sshd's pw_shell now names (ovmx_sshd_loginout_path resolves + # SYS$SYSTEM:LOGINOUT.EXE). OpenSSH's allowed_user() stat()s pw_shell at auth + # time, so a valid SYSUAF user is REJECTED before the SYSUAF read unless this + # file exists -- test_syssvc_ssh_server's accept half. The handoff never + # execs it in a bare KE process (it $CREPRC's LOGINOUT, which fails closed + # with no /run/ovmx-boot staging); this only satisfies the shell-existence + # precondition, the faithful analogue of the DCL.EXE staging above. LOWERCASE + # basename to match vmsfs_to_linux_path's translation of the .EXE filename + # (the exact path the wrapped sshd stat()s: /vms/.../SYSEXE/loginout.exe). + { cp /src/repo/build-static/bin/LOGINOUT.EXE /initramfs/vms/SYS0/SYSCOMMON/SYSEXE/loginout.exe || \ + { echo "FATAL: LOGINOUT.EXE was not built into the static tree -- the wrapped sshd's pw_shell existence check (test_syssvc_ssh_server accept half) would reject every SYSUAF user"; exit 1; }; } && \ + chmod +x /initramfs/vms/SYS0/SYSCOMMON/SYSEXE/loginout.exe && \ # TCPIP$DAYTIME.EXE at SYS$SYSTEM (vms-477) -- the REAL standalone RFC 867 # service image the auxiliary server launches; test_syssvc_tcpip_daytime.c # execs it from this path (OVMX_DAYTIME default diff --git a/tests/qemu/test_kmod_devtab_terminal.c b/tests/qemu/test_kmod_devtab_terminal.c index a9b0aae70..27c0d5dd5 100644 --- a/tests/qemu/test_kmod_devtab_terminal.c +++ b/tests/qemu/test_kmod_devtab_terminal.c @@ -64,6 +64,7 @@ #define SS_NORMAL 1 #define SS_IVCHAN 602 +#define SS_IVDEVNAM 608 #define SS_NOSUCHDEV 2680 #define DC_TERM 66 /* DC$_TERM */ @@ -124,6 +125,8 @@ struct owner_report { uint32_t own_vms_pid; uint32_t assign_status; uint32_t chan; + uint32_t netlogin_status; /* VMS_IOCTL_TERM_GETLOGIN status */ + char netlogin[VMS_USERNAME_SIZE]; /* the note B stamped, read by A */ }; static int process_a(int wfd) @@ -143,6 +146,13 @@ static int process_a(int wfd) rep.assign_status = vms_kif_assign(RTA_DEV, &rep.chan); + /* vms-65b: read back the network-login note B stamped on RTA0: BEFORE + * forking us -- from A's OWN fresh /dev/vms fd. If the note lived in B's + * process memory rather than the shared executive device table, A would + * read nothing here (the §7.5 anti-LARP bind, applied to the note). */ + rep.netlogin_status = vms_kif_terminal_getlogin(RTA_DEV, rep.netlogin, + sizeof(rep.netlogin)); + if (write(wfd, &rep, sizeof(rep)) != (ssize_t)sizeof(rep)) return 1; @@ -260,6 +270,48 @@ int main(int argc, char **argv) "RTA0: carries OPA0:'s width/page (132x24)"); CHECK(info.owner_pid == 0, "RTA0: starts unowned -- ownership is not stamped at creation"); + /* -------------------------------------------------------------- + * 2b. THE NETWORK-LOGIN PRE-AUTH NOTE (rd vms-65b). A daemon vouches a + * pre-authenticated user onto the RTAn: it minted (VMS_IOCTL_TERM_ + * SETLOGIN); the $CREPRC(LOGINOUT) child bound to that terminal reads + * it back (VMS_IOCTL_TERM_GETLOGIN) and skips the prompt. Prove the + * round trip through the REAL executive, the honest-omission floor, + * and the guards -- then the cross-process read is proven in §3 below. + * -------------------------------------------------------------- */ + { + char note[VMS_USERNAME_SIZE]; + + /* Fresh RTA0: carries no note -> empty, honest omission (NOT an error: + * an ordinary terminal authenticates its own user). LOGINOUT reads this + * as "no network pre-auth" and prompts (fail-closed, INV-6). */ + memset(note, 0xAA, sizeof(note)); + status = vms_kif_terminal_getlogin(RTA_DEV, note, sizeof(note)); + CHECK(status == SS_NORMAL && note[0] == '\0', + "a fresh RTA0: has no network-login note (honest omission, not error)"); + + /* Stamp the pre-authenticated user (we are root here -> privileged). */ + status = vms_kif_terminal_setlogin(RTA_DEV, "SYSTEM"); + CHECK(status == SS_NORMAL, + "VMS_IOCTL_TERM_SETLOGIN stamps the pre-authenticated user onto RTA0:"); + + /* Read it back, same process, THROUGH /dev/vms (not process memory). */ + memset(note, 0, sizeof(note)); + status = vms_kif_terminal_getlogin(RTA_DEV, note, sizeof(note)); + CHECK(status == SS_NORMAL && strcmp(note, "SYSTEM") == 0, + "VMS_IOCTL_TERM_GETLOGIN reads the stamped note back from the executive"); + + /* The console (OPA0:, a static non-dynamic terminal) is not a legal + * target for a network-login note -- the same IVDEVNAM category guard + * RESOLVE gives, so a daemon cannot vouch a user onto the operator's + * console. */ + status = vms_kif_terminal_setlogin(CONSOLE_DEV, "SYSTEM"); + CHECK(status == SS_IVDEVNAM, + "SETLOGIN refuses OPA0: (a note belongs only on a dynamic RTAn:)"); + status = vms_kif_terminal_getlogin(CONSOLE_DEV, note, sizeof(note)); + CHECK(status == SS_IVDEVNAM, + "GETLOGIN refuses OPA0: (the console carries no network-login note)"); + } + /* -------------------------------------------------------------- * 3. A DIFFERENT PROCESS takes RTA0:'s only channel. This is the * §7.5 tell: the device must be real BEFORE this process ever asks @@ -300,6 +352,14 @@ int main(int argc, char **argv) CHECK(rep.assign_status == SS_NORMAL && rep.chan != 0, "a DIFFERENT process ($ASSIGN from a fresh /dev/vms fd) takes RTA0:'s channel"); + /* vms-65b: process A, from its OWN /dev/vms fd, read back the note B + * stamped on RTA0: before the fork. This is the §7.5 anti-LARP bind for + * the conveyance: a note held in B's process memory would come back empty + * here. It comes back "SYSTEM" -- the SSH-daemon-stamps / LOGINOUT-reads + * path, proven cross-process through the shared executive device table. */ + CHECK(rep.netlogin_status == SS_NORMAL && strcmp(rep.netlogin, "SYSTEM") == 0, + "process A reads B's network-login note on RTA0: cross-process (B writes, A reads)"); + /* B reads RTA0: back cold -- nothing above told B what A's pid is * except the bare integer over the pipe (the SAME provenance * test_kmod_devtab.c uses for OPA0:). */ diff --git a/tests/qemu/test_syssvc_ssh_server.c b/tests/qemu/test_syssvc_ssh_server.c index c9852916c..7ba6ea3d7 100644 --- a/tests/qemu/test_syssvc_ssh_server.c +++ b/tests/qemu/test_syssvc_ssh_server.c @@ -22,11 +22,14 @@ * * It then drives BOTH SYSUAF/Purdy password outcomes over that same wrapped sshd: * an unknown user is REFUSED (fail-closed, needs only /dev/vms), and -- the RUNG-3 - * step 3d capstone (rd vms-9cc) -- a VALID SYSUAF user (SYSTEM/MANAGER) authenticates - * by PASSWORD and LANDS IN a real interactive DCL session. The wrapped sshd resolves - * the REAL shipped SYS$SYSTEM:SYSUAF.DAT over the executive ACP on the real system - * volume VDA300: (see password_login_of_valid_user_lands_in_dcl) -- no /etc/passwd - * seed, no stubbed lookup; the SYSUAF resolution is genuinely exercised. + * step 3d (rd vms-9cc) -- a VALID SYSUAF user (SYSTEM/MANAGER) authenticates by + * PASSWORD. The wrapped sshd resolves the REAL shipped SYS$SYSTEM:SYSUAF.DAT over + * the executive ACP on the real system volume VDA300: (see + * password_login_of_valid_user_authenticates) -- no /etc/passwd seed, no stubbed + * lookup; the SYSUAF resolution is genuinely exercised. The session then hands + * off to $CREPRC(LOGINOUT) (vms-16b); landing that in DCL needs the booted + * image's boot-staging, so the end-to-end "ssh SYSTEM@ -> DCL $" is proven on the + * booted-distro ssh-boot-e2e (vms-843a), not this bare test. * * Honest Rule-9 skip: the wrapped sshd needs /dev/vms to bind over BGn:; with no * executive it cannot start, so the proof is honestly skipped, never faked. @@ -172,6 +175,31 @@ static void dump_sshd_log(void) fclose(f); } +/* Does the wrapped sshd's -e log contain `needle`? Scans the WHOLE file line by + * line -- NOT a fixed head-buffer read. Under LogLevel DEBUG1 across several + * connections SSHD_LOG runs to many KB of debug output, and the auth-result line + * we look for ("Accepted password for SYSTEM") belongs to the LAST connection, + * so it sits far past the start; a bounded head read would miss it entirely. + * The sshd's own auth-result lines are the executive-independent record of which + * SYSUAF outcome the wrapped sshd reached. */ +static int sshd_log_contains(const char *needle) +{ + FILE *f = fopen(SSHD_LOG, "r"); + char line[1024]; + int found = 0; + + if (!f) + return 0; + while (fgets(line, sizeof(line), f)) { + if (strstr(line, needle) != NULL) { + found = 1; + break; + } + } + fclose(f); + return found; +} + /* * vms-0cd RUNG-3 step 3c: the wrapped sshd now authenticates PASSWORDS against * the BINARY SYSUAF (Purdy) via our sys_auth_passwd shim, selected at build with @@ -180,8 +208,8 @@ static void dump_sshd_log(void) * linked). This drives the negative, runtime-light half of the proof end to * end: a PASSWORD login for a user with NO SYSUAF record must be REJECTED * (fail-closed, INV-6). It needs only /dev/vms -- no provisioned SYS$SYSTEM: / - * DCL.EXE. The positive "valid SYSUAF user -> lands in DCL" half is the 3d capstone - * in password_login_of_valid_user_lands_in_dcl() below, which provisions the real + * DCL.EXE. The positive "valid SYSUAF user is password-accepted" half is + * password_login_of_valid_user_authenticates() below, which provisions the real * system volume so SYS$SYSTEM:SYSUAF.DAT resolves over the ACP. Returns 1 if the * unknown-user password login was correctly refused, 0 if it slipped through. */ @@ -237,10 +265,15 @@ static int password_login_of_unknown_user_is_refused(void) } /* - * vms-0cd RUNG-3 step 3d (rd vms-9cc): the POSITIVE capstone -- a VALID SYSUAF - * user (SYSTEM/MANAGER) authenticates by PASSWORD over the wrapped OpenSSH sshd - * and LANDS IN a real interactive DCL session over the executive BGn: connection, - * no AF_UNIX socketpair. + * vms-0cd RUNG-3 step 3d (rd vms-9cc), re-scoped for the vms-16b handoff: a + * VALID SYSUAF user (SYSTEM/MANAGER) authenticates by PASSWORD over the wrapped + * OpenSSH sshd -- the wrapped sshd opens the REAL SYS$SYSTEM:SYSUAF.DAT over the + * executive ACP and Purdy-verifies. The session then hands off to + * $CREPRC(LOGINOUT) (src/vmsssh/sshd_session.c); LANDING that in DCL needs the + * booted image's boot-staging (see the caller in main()), so this bare test + * proves the AUTH half (via the sshd -e log) and the booted ssh-boot-e2e + * (vms-843a) proves lands-in-DCL. Drives the login end to end so the real ACP + * SYSUAF read is genuinely exercised; captures the client output for diagnosis. * * Provisioning (the real-ACP part, INV-6): the wrapped sshd's __wrap_getpwnam -> * sysuaf_lookup opens SYS$SYSTEM:SYSUAF.DAT over the Files-11 ACP. The KE harness's @@ -259,17 +292,15 @@ static int password_login_of_unknown_user_is_refused(void) * NO /etc/passwd seed for SYSTEM, NO stubbed lookup; the resolution that was never * runtime-exercised before is genuinely driven here. * - * The wrapped sshd verifies SYSTEM/MANAGER (SYSUAF/Purdy), __wrap_getpwnam sets - * pw_shell=DCL, ovmx_sshd_pre_drop_pw establishes the executive identity (or emits - * %OVMX-F-NOIDENT + exit, fail-closed) then prints the SYS$WELCOME banner, and - * __wrap_execve rewrites do_child's login-shell exec into `vmsdcl --login`. We feed - * a DCL command + LOGOUT on the session stdin and read the reply back; the marker - * in the client's stdout proves the command crossed the wrapped BGn: session channel - * into DCL's own interpreter (non-interactive DCL uses fgets with NO echo, so the - * marker proves execution, not an echoed input line). Captures the client's combined - * stdout/stderr into out[] for main()'s assertions. + * The wrapped sshd verifies SYSTEM/MANAGER (SYSUAF/Purdy) and logs "Accepted + * password for SYSTEM" (main() asserts on that -- the executive-independent + * record that the ACP SYSUAF read accepted the real user). ovmx_sshd_pre_drop_pw + * then hands the session off to $CREPRC(LOGINOUT), which in this bare env fails + * closed (no /run/ovmx-boot staging). We still feed a DCL command + LOGOUT on the + * session stdin and capture the client output into out[] for diagnosis (the + * landed-DCL marker is asserted on the booted ssh-boot-e2e, not here). */ -static void password_login_of_valid_user_lands_in_dcl(char *out, size_t outsz) +static void password_login_of_valid_user_authenticates(char *out, size_t outsz) { const char *askpass = "/tmp/ovmx_askpass_ok"; const char *dclcmd = "WRITE SYS$OUTPUT \"OVMX_DCL_LANDED_9cc\"\nLOGOUT\n"; @@ -475,44 +506,62 @@ int main(void) * (ovmx_sshd_rms_bind.o), the wrapped sshd's SYSUAF reader was NULL and * refused EVERY user without opening the file -- so this passed vacuously. The * positive assertion below (a REAL SYSUAF user SYSTEM is Purdy-authenticated - * and lands in DCL over the SAME sshd) is what proves the reader actually + * and PASSWORD-ACCEPTED over the SAME sshd) is what proves the reader actually * opened SYSUAF; with that green, this refusal is genuinely "user absent from * a read SYSUAF," not "reader inert." The two are asserted together. */ CHECK(password_login_of_unknown_user_is_refused(), "a PASSWORD login for a user with no SYSUAF record is REFUSED by the wrapped sshd -- SYSUAF/Purdy auth fails closed (non-vacuous: the SYSTEM positive below proves the reader opened SYSUAF) (vms-0cd 3c / vms-9cc / INV-6)"); - /* vms-0cd 3d (rd vms-9cc): the POSITIVE capstone -- a VALID SYSUAF user - * (SYSTEM/MANAGER) authenticates by PASSWORD over the wrapped sshd and lands - * in a real interactive DCL session over the BGn: connection. */ + /* vms-0cd 3d (rd vms-9cc), RE-SCOPED for the vms-16b LOGINOUT handoff: the + * POSITIVE half -- a VALID SYSUAF user (SYSTEM/MANAGER) is PASSWORD- + * authenticated (Purdy) against the REAL shipped SYS$SYSTEM:SYSUAF.DAT over + * the executive ACP by the wrapped sshd. This is the non-vacuity partner of + * the unknown-user refusal above: it proves the SYSUAF reader ACTUALLY + * OPENED THE FILE and accepted a real user, so the refusal above is "user + * absent from a read SYSUAF", not "reader inert". + * + * WHY THIS NO LONGER ASSERTS "LANDS IN DCL" HERE (vms-16b / vms-29b). The + * wrapped sshd's session handoff was retired from a raw execve of DCL.EXE to + * $CREPRC(LOGINOUT.EXE, RTAn:, PRC$M_INTER|PRC$M_LOGINOUT) + * (src/vmsssh/sshd_session.c). Activating LOGINOUT.EXE needs PID 1's + * /run/ovmx-boot staging (LOGINOUT + IMGACT + the shareable chain off ODS-2 + * over the ACP), which a BARE kernel-executive process does not have -- so + * here the handoff correctly FAILS CLOSED (%OVMX-F-NOSESSION, no fabricated + * session) rather than landing DCL. That is BY DESIGN: the tree's own + * doctrine (tests/integration/test_creprc_session_primitive.sh) puts the + * runtime landed-session proof on the BOOTED image, not a bare test. The + * end-to-end "ssh SYSTEM@ -> DCL $" (marker OVMX_DCL_LANDED_843a) is proven + * on the booted-distro ssh-boot-e2e (rd vms-843a, PR #1128) -- the ssh + * analogue of the console proof in tests/qemu/lib/dcl_acceptance_battery.sh + * and the live DECnet SET HOST proof. Nothing goes unproven: transport + + * auth + handoff-initiation in-process here, lands-in-DCL on the booted image. + */ { char dbuf[4096]; - password_login_of_valid_user_lands_in_dcl(dbuf, sizeof(dbuf)); - - /* The capstone gate: a DCL command fed on the session stdin executed and - * its output returned -- only a real SYSUAF-authenticated login that reached - * DCL's interpreter produces this (empty/failed sessions have no marker). */ - CHECK(strstr(dbuf, "OVMX_DCL_LANDED_9cc") != NULL, - "a DCL command fed on the SSH session stdin executed and its output returned -- a valid SYSUAF user (SYSTEM/MANAGER, resolved from the real SYS$SYSTEM:SYSUAF.DAT over the ACP) landed in a real interactive DCL session over the wrapped BGn: connection (vms-9cc capstone)"); - CHECK(strstr(dbuf, "OVMX-F-NOIDENT") == NULL, - "the executive did NOT refuse the SYSTEM identity -- SYSUAF/Purdy password auth + executive $SETIDENT succeeded, fail-closed (vms-0cd 3c / INV-6)"); - - /* SYS$WELCOME banner: NON-GATING diagnostic, not an assertion. This - * session is NON-PTY (a pipe on stdin), and ovmx_sshd_pre_drop_pw emits - * the banner to stdout from inside __wrap_permanently_set_uid, which - * OpenSSH's do_child runs BEFORE it wires the session channel onto fd1 for - * a non-PTY exec -- so the banner lands on the pre-dup2 fd, not the client - * channel (DCL's later output, post channel-setup, does reach the client: - * the marker above proves it). Faithfully, VMS shows SYS$WELCOME for an - * INTERACTIVE (PTY) login, not a piped/non-interactive session, so gating - * it here would assert non-faithful behaviour. The DCL-landing marker + - * no-NOIDENT are the capstone proof. A faithful PTY-banner proof (ssh -tt - * + an echo-proof computed marker) is a separate follow-up. */ - printf(" [9cc-diag] SYS$WELCOME banner in non-PTY session stdout: %s " - "(non-gating; banner is an interactive/PTY-login artifact)\n", - strstr(dbuf, "Welcome to") != NULL ? "present" : "absent"); - - if (strstr(dbuf, "OVMX_DCL_LANDED_9cc") == NULL) { - printf(" --- valid-user (SYSTEM) DCL session stdout: [%s] ---\n", dbuf); + int accepted = 0; + int tries; + + password_login_of_valid_user_authenticates(dbuf, sizeof(dbuf)); + + /* Scan the WHOLE sshd log for the acceptance (sshd_log_contains reads it + * line by line -- the DEBUG1 log is many KB and the SYSTEM accept is the + * LAST connection's line, far past any head-buffer). The bounded retry is + * belt-and-suspenders against the persistent -D daemon flushing the line + * a beat after the client exits; the accept is logged during auth so it + * is normally present on the first scan. */ + for (tries = 0; tries < 300; tries++) { + if (sshd_log_contains("Accepted password for SYSTEM")) { + accepted = 1; + break; + } + usleep(10000); /* 10ms; 300 * 10ms = 3s cap, well under the alarm */ + } + + CHECK(accepted, + "a VALID SYSUAF user (SYSTEM) is PASSWORD-authenticated (Purdy) by the wrapped sshd against the real SYS$SYSTEM:SYSUAF.DAT over the ACP -- the non-vacuity partner of the unknown-user refusal (proves the reader opened SYSUAF); lands-in-DCL is proven on the booted ssh-boot-e2e (vms-843a)"); + + if (!accepted) { + printf(" --- valid-user (SYSTEM) client stdout: [%s] ---\n", dbuf); dump_sshd_log(); } } diff --git a/tools/vms_login.c b/tools/vms_login.c index bd110cc6c..84aa9c743 100644 --- a/tools/vms_login.c +++ b/tools/vms_login.c @@ -1012,6 +1012,87 @@ static int run_network_permit_selftest(void) return fail > 0 ? 1 : 0; } +/* ------------------------------------------------------------------ */ +/* NETWORK-LOGIN SESSION ESTABLISH (vms-843a / vms-65b) */ +/* */ +/* The live half of loginout_network_permit above: a LOGINOUT created */ +/* by $CREPRC(LOGINOUT, RTAn:, PRC$M_INTER|PRC$M_LOGINOUT) for an */ +/* inbound network session (SSH today; the daemon minted the RTAn: and */ +/* stamped the pre-authenticated user onto it, rd vms-65b). Instead of */ +/* prompting Username:/Password:, LOGINOUT reads the note off ITS OWN */ +/* terminal, trusts the daemon's in-protocol authentication (Option A -- */ +/* SSH already authed the SAME SYSUAF/Purdy authority), and hands over */ +/* the EXACT SAME start_session() the console login uses. The persona is */ +/* still built from the binary SYSUAF record, so a permitted user gets */ +/* precisely its own UIC/privileges and nothing more. */ +/* ------------------------------------------------------------------ */ + +/* + * Read the network pre-authentication note off this LOGINOUT's own terminal. + * Returns 1 and fills *user (upcased SYSUAF key) when a daemon stamped a + * non-empty note on the RTAn: this session was created on; 0 otherwise -- + * which is the ordinary console/DECnet case (no note), the honest fail-closed + * signal to fall back to the interactive prompt (INV-6). + */ +static int network_login_conveyed(char *user, size_t usz) +{ + struct vms_procinfo pi; + char note[VMS_USERNAME_SIZE]; + + if (user == NULL || usz == 0) + return 0; + + /* Which terminal is this session on? The executive records it when + * $CREPRC binds the process (creprc_bind_terminal -> SETTERM); a session + * created on an RTAn: reports that name, a bare LOGINOUT.EXE at a shell + * reports none. Only a real terminal-bound session can carry a note. */ + memset(&pi, 0, sizeof(pi)); + if (!(vms_kif_getjpi_self(&pi) & 1) || pi.terminal[0] == '\0') + return 0; + + /* Read the note for THAT terminal. Absent/empty -> no network pre-auth + * (an ordinary console or DECnet SET HOST terminal, which authenticates + * its user itself): prompt, do not fabricate an identity. */ + note[0] = '\0'; + if (!(vms_kif_terminal_getlogin(pi.terminal, note, sizeof(note)) & 1) + || note[0] == '\0') + return 0; + + snprintf(user, usz, "%s", note); + return 1; +} + +/* + * Establish the network session for the pre-authenticated `username`. Reads the + * binary SYSUAF record, applies the fail-closed network-login authorization + * (loginout_network_permit -- DISUSER/DISACNT/DISNETWORK/expired all REFUSE), + * and on permit hands over to start_session() (identity stamp + credential drop + * + DCL activation -- the SAME primitive, no password re-read). Returns on + * REFUSAL only (unknown user, unreadable SYSUAF, or a login-flag denial): the + * caller then ends the session, which is the fail-closed outcome -- the SSH + * transport already consumed the auth exchange, so there is nothing to prompt + * for and admitting the peer anyway would be the illegal third answer (Rule 10). + * On success start_session() execs DCL and never returns here. + */ +static int network_login(const char *username) +{ + sysuaf_record_t rec; + uint32_t uaf_st = 0; + + memset(&rec, 0, sizeof(rec)); + if (username == NULL || username[0] == '\0') + return 0; + if (sysuaf_lookup_st(username, &rec, &uaf_st) < 0) + return 0; /* unknown / unreadable -> refuse */ + if (!loginout_network_permit(&rec)) + return 0; /* DISUSER/DISNETWORK/expired -> refuse */ + + /* 0 failures: a pre-authenticated network session made no bad-password + * attempts against LOGINOUT (it never prompted). */ + start_session(&rec, 0); /* stamps identity, drops, execs DCL */ + return 0; /* not reached */ +} + /* ------------------------------------------------------------------ */ /* Main */ /* ------------------------------------------------------------------ */ @@ -1029,5 +1110,21 @@ int main(int argc, char *argv[]) vmsfs_device_add(SYSDISK_DEVICE, SYSDISK_MOUNT); lnm_setup_defaults(lnm_get_manager(), SYSDISK_MOUNT); + /* + * NETWORK LOGIN (vms-843a / vms-65b). If an inbound network daemon + * pre-authenticated a user and stamped it on this session's terminal + * (SSH: $CREPRC(LOGINOUT, RTAn:) after in-protocol SYSUAF auth), trust it + * and skip the prompt -- LOGINOUT is the ONE session-establish primitive + * for console, DECnet SET HOST and SSH. Absent (the common case) falls + * through to the interactive console prompt loop below, unchanged: the + * operator console and DECnet SET HOST both re-challenge on their own + * terminal, so their sessions carry no note and reach console_login(). + */ + { + char netuser[VMS_USERNAME_SIZE]; + if (network_login_conveyed(netuser, sizeof(netuser))) + return network_login(netuser); + } + return console_login(); }