From ddf95c206d0253c96afcf465767dfc03ad037ed1 Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 10 Aug 2026 10:37:25 +0000 Subject: [PATCH 1/3] dcl: MOUNT/DISMOUNT really mount(2)/umount(2) vmsfs volumes (vms-651) KILLED the cmd_mount/cmd_dismount facade (src/vmsdcl/dcl_cmd_misc.c:1597): it never called mount(2), wrote a per-process userspace device table (struct vms_device / vms_device_table[], src/vmsdcl/dcl_builtin.c -- deleted, its only two readers), used getcwd() as the "mount path", and printed %MOUNT-I-MOUNTED unconditionally. MOUNT DKA100: now: checks PRV$M_MOUNT through the executive (vms_kif_chkpriv, not getuid() -- PRV$M_MOUNT is a new privilege bit, oracle-pinned public $PRVDEF position 17, wired into VMS_PRV_M_ENFORCED); resolves the unit's backing block device through the executive (vms_kif_disk_resolve, vms-3e8); claims the unit in the executive's device table (vms_kif_alloc, wired here for the first time, so a second process cannot mount the same unit concurrently); and mount(2)s "/dev/" as vmsfs at the unit's mount point. DISMOUNT is the mirror image with umount(2). "Already mounted" is answered from the kernel's own /proc/mounts, not a per-process field. THE PRIVILEGE-DROP WRINKLE, measured rather than assumed: LOGINOUT setuid()/setgid()'s every VMS session onto its SYSUAF UIC (tools/ vms_login.c), so by the time MOUNT runs the calling process holds no Linux capability regardless of PRV$M_MOUNT -- mount(2)/umount(2) require CAP_SYS_ADMIN unconditionally. A kernel-mediated mount (a vms.ko ioctl calling the mount-syscall internals with elevated creds) was tried and does NOT work on this platform: fs/namespace.c's do_mount()/path_mount()/ do_umount()/path_umount() are absent from this kernel's exported-symbol table entirely (confirmed against the real Module.symvers, not guessed), and the public fs_context replacement API has no exported way to graft a mount onto a path in a process's namespace. The settled design is the classic Unix shape instead: tools/vms_mount_helper.c, a small setuid-root binary that does only mount(2)/umount(2) and re-derives ITS OWN authorization from the executive (vms_kif_register_continue() +chkpriv) before doing anything privileged, so it never trusts its caller -- direct invocation registers as a fresh, unauthenticated process and is refused. cmd_mount/cmd_dismount fork, continue this session's identity onto the child, and exec the helper. Mount points are pre-created as root by src/ovmx_init/ovmx_init.c's new provision_disk_mount_points() (enumerating DC$_DISK units via the SAME vms_kif_devscan() table MOUNT reads) before any session drops privilege -- mkdir(2) is a DAC check a de-privileged session can't satisfy against root-owned /mnt either. Ground-source proof (tests/qemu/test_mount_e2e.sh + run_mount_e2e.sh, real vms.ko+vmsfs.ko, opt-in OVMX_QEMU_FULL_E2E=1): MOUNT DKA100:, COPY a real file onto it, DISMOUNT, re-MOUNT -> file still there; /proc/mounts shows the vmsfs mount while mounted and not after DISMOUNT; a full QEMU process restart against the same disk file -> the file survives, byte-identical (proves the disk, not a tmpfs); MOUNT of a nonexistent unit (DKA200:) fails honestly with no %MOUNT-I-MOUNTED. 17/17 assertions pass. tests/dcl/ test_mount.sh and test_show_device.sh (ctest, no /dev/vms) are rewritten to assert the honest-failure path instead of the deleted facade's success message -- MOUNT's own liveness anchor changed from "the facade always succeeds" to a bare WRITE, since MOUNT now asks the executive too and fails the same way SHOW DEVICE does with no executive present. Co-Authored-By: Claude Opus 4.8 (1M context) --- distro/Dockerfile.bootable | 16 + src/imgact/test/run_libvms_native.sh | 2 +- src/imgact/test/run_tcc_native.sh | 2 +- src/imgact/test/run_tcc_object_native.sh | 2 +- src/imgact/test/run_tcc_rms.sh | 2 +- src/imgact/test/run_tcc_selfhost.sh | 2 +- src/imgact/test/run_vmsrms_native.sh | 2 +- src/kernel/vms_ioctl.h | 23 +- src/libvms/include/ovmx_layout.h | 10 + src/libvms/prv_agreement.c | 2 + src/libvmssys/vms_kif.h | 38 ++- src/ovmx_init/ovmx_init.c | 54 ++++ src/vmsdcl/dcl_builtin.c | 36 --- src/vmsdcl/dcl_cmd_misc.c | 363 +++++++++++++++++++---- src/vmsdcl/include/dcl/dcl_cmd.h | 15 - src/vmslink/mk_dcl.sh | 8 +- src/vmslink/mk_vmssys_shr.sh | 2 +- tests/dcl/test_mount.sh | 54 +++- tests/dcl/test_show_device.sh | 55 ++-- tests/qemu/CMakeLists.txt | 20 ++ tests/qemu/run_mount_e2e.sh | 87 ++++++ tests/qemu/test_mount_e2e.sh | 332 +++++++++++++++++++++ tools/CMakeLists.txt | 32 ++ tools/vms_mount_helper.c | 107 +++++++ 24 files changed, 1103 insertions(+), 163 deletions(-) create mode 100755 tests/qemu/run_mount_e2e.sh create mode 100755 tests/qemu/test_mount_e2e.sh create mode 100644 tools/vms_mount_helper.c diff --git a/distro/Dockerfile.bootable b/distro/Dockerfile.bootable index c95bd1c18..5dbd693aa 100644 --- a/distro/Dockerfile.bootable +++ b/distro/Dockerfile.bootable @@ -183,6 +183,7 @@ RUN cmake -B build-static \ # OWNER category) can sys$create scratch/user files in them. RUN mkdir -p /initramfs-fat/dev /initramfs-fat/proc /initramfs-fat/sys \ /initramfs-fat/tmp /initramfs-fat/var /initramfs-fat/mnt \ + /initramfs-fat/sbin \ /initramfs-fat/lib/modules \ /initramfs-fat/vms/SYS0/SYSCOMMON/SYSEXE \ /initramfs-fat/vms/SYS0/SYSCOMMON/SYSLIB \ @@ -223,6 +224,14 @@ RUN mkdir -p /initramfs-fat/dev /initramfs-fat/proc /initramfs-fat/sys \ # command fails %SYSGEN-F-NOIMG and SYS$SYSTEM:OVMXVMSSYS.PAR can never # be read or written on the real runtime. cp build-static/bin/SYSGEN.EXE /initramfs-fat/vms/SYS0/SYSCOMMON/SYSEXE/ 2>/dev/null ; \ + # The setuid-root mount(2)/umount(2) helper (vms-651, tools/ + # vms_mount_helper.c) -- NOT a VMS image, lives outside SYS$SYSTEM: at a + # fixed Linux path (ovmx_layout.h VMS_MOUNT_HELPER_PATH). Owned by root + # with the setuid bit set HERE, at build time, so no boot-time step ever + # has to (a de-privileged session couldn't set it on itself anyway). + cp build-static/bin/vms_mount_helper /initramfs-fat/sbin/vms_mount_helper && \ + chown root:root /initramfs-fat/sbin/vms_mount_helper && \ + chmod 4755 /initramfs-fat/sbin/vms_mount_helper && \ # Kernel modules cp src/kernel/vms.ko /initramfs-fat/lib/modules/ && \ cp src/kernel/vmsfs/vmsfs.ko /initramfs-fat/lib/modules/ && \ @@ -400,12 +409,19 @@ RUN /src/build-static/bin/vmsfs_master master \ # Use this after the system disk has been installed via the fat initramfs. RUN mkdir -p /initramfs-slim/dev /initramfs-slim/proc /initramfs-slim/sys \ /initramfs-slim/tmp /initramfs-slim/var /initramfs-slim/mnt \ + /initramfs-slim/sbin \ /initramfs-slim/lib/modules \ /initramfs-slim/vms/SYS0/SYSCOMMON/SYSEXE \ /initramfs-slim/vms/SYS0/SYSCOMMON/SYSMGR && \ # Bootstrap binaries only cp build-static/bin/STARTUP.EXE /initramfs-slim/init && \ cp build-static/bin/INITIALIZE.EXE /initramfs-slim/vms/SYS0/SYSCOMMON/SYSEXE/ 2>/dev/null ; \ + # MOUNT/DISMOUNT (vms-651) need this on a slim boot too -- DCL comes off + # the system disk there, but the helper still lives at the same fixed + # Linux path outside the VMS tree. + cp build-static/bin/vms_mount_helper /initramfs-slim/sbin/vms_mount_helper && \ + chown root:root /initramfs-slim/sbin/vms_mount_helper && \ + chmod 4755 /initramfs-slim/sbin/vms_mount_helper && \ # Kernel modules cp src/kernel/vms.ko /initramfs-slim/lib/modules/ && \ cp src/kernel/vmsfs/vmsfs.ko /initramfs-slim/lib/modules/ && \ diff --git a/src/imgact/test/run_libvms_native.sh b/src/imgact/test/run_libvms_native.sh index 12a666145..261bbf16e 100755 --- a/src/imgact/test/run_libvms_native.sh +++ b/src/imgact/test/run_libvms_native.sh @@ -124,7 +124,7 @@ done $CC -fPIC -mno-outline-atomics -c -o "$WORK/sys_syscall.o" "$LIBVMSSYS_DIR/arch/aarch64/syscall.S" SYSOBJS="$SYSOBJS $WORK/sys_syscall.o" "$WORK/LINK.EXE" --shareable \ - --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE" \ + --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE,vms_kif_chkpriv=PROCEDURE,vms_kif_alloc=PROCEDURE,vms_kif_dalloc=PROCEDURE" \ --gsmatch LEQUAL,1,0 -o "$SYSLIB/LIBVMSSYS\$SHR.EXE" $SYSOBJS readelf -SW "$SYSLIB/LIBVMSSYS\$SHR.EXE" | grep -q '\.vms\$sv' || { echo "FAIL: LIBVMSSYS\$SHR no symbol vector"; exit 1; } diff --git a/src/imgact/test/run_tcc_native.sh b/src/imgact/test/run_tcc_native.sh index 6208458d6..1409bdae0 100755 --- a/src/imgact/test/run_tcc_native.sh +++ b/src/imgact/test/run_tcc_native.sh @@ -95,7 +95,7 @@ done $CC -fPIC -mno-outline-atomics -c -o "$WORK/sys_syscall.o" "$LIBVMSSYS_DIR/arch/aarch64/syscall.S" SYSOBJS="$SYSOBJS $WORK/sys_syscall.o" "$WORK/LINK.EXE" --shareable \ - --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE" \ + --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE,vms_kif_chkpriv=PROCEDURE,vms_kif_alloc=PROCEDURE,vms_kif_dalloc=PROCEDURE" \ --gsmatch LEQUAL,1,0 -o "$SYSLIB/LIBVMSSYS\$SHR.EXE" $SYSOBJS echo "== LIBVMSPROCESS\$SHR.EXE ==" diff --git a/src/imgact/test/run_tcc_object_native.sh b/src/imgact/test/run_tcc_object_native.sh index 39eef40e5..9592b4b2f 100755 --- a/src/imgact/test/run_tcc_object_native.sh +++ b/src/imgact/test/run_tcc_object_native.sh @@ -114,7 +114,7 @@ done $CC -fPIC -mno-outline-atomics -c -o "$WORK/sys_syscall.o" "$LIBVMSSYS_DIR/arch/aarch64/syscall.S" SYSOBJS="$SYSOBJS $WORK/sys_syscall.o" "$WORK/LINK.EXE" --shareable \ - --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE" \ + --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE,vms_kif_chkpriv=PROCEDURE,vms_kif_alloc=PROCEDURE,vms_kif_dalloc=PROCEDURE" \ --gsmatch LEQUAL,1,0 -o "$SYSLIB/LIBVMSSYS\$SHR.EXE" $SYSOBJS echo "== LIBVMSPROCESS\$SHR.EXE ==" diff --git a/src/imgact/test/run_tcc_rms.sh b/src/imgact/test/run_tcc_rms.sh index 032477e0f..ba74668e1 100755 --- a/src/imgact/test/run_tcc_rms.sh +++ b/src/imgact/test/run_tcc_rms.sh @@ -103,7 +103,7 @@ done $CC -fPIC -mno-outline-atomics -c -o "$WORK/sys_syscall.o" "$LIBVMSSYS_DIR/arch/aarch64/syscall.S" SYSOBJS="$SYSOBJS $WORK/sys_syscall.o" "$WORK/LINK.EXE" --shareable \ - --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE" \ + --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE,vms_kif_chkpriv=PROCEDURE,vms_kif_alloc=PROCEDURE,vms_kif_dalloc=PROCEDURE" \ --gsmatch LEQUAL,1,0 -o "$SYSLIB/LIBVMSSYS\$SHR.EXE" $SYSOBJS echo "== LIBVMSPROCESS\$SHR.EXE ==" diff --git a/src/imgact/test/run_tcc_selfhost.sh b/src/imgact/test/run_tcc_selfhost.sh index 1cb5f00fc..db3d07064 100644 --- a/src/imgact/test/run_tcc_selfhost.sh +++ b/src/imgact/test/run_tcc_selfhost.sh @@ -122,7 +122,7 @@ done $CC -fPIC -mno-outline-atomics -c -o "$WORK/sys_syscall.o" "$LIBVMSSYS_DIR/arch/aarch64/syscall.S" SYSOBJS="$SYSOBJS $WORK/sys_syscall.o" "$WORK/LINK.EXE" --shareable \ - --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE" \ + --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE,vms_kif_chkpriv=PROCEDURE,vms_kif_alloc=PROCEDURE,vms_kif_dalloc=PROCEDURE" \ --gsmatch LEQUAL,1,0 -o "$SYSLIB/LIBVMSSYS\$SHR.EXE" $SYSOBJS echo "== LIBVMSPROCESS\$SHR.EXE ==" diff --git a/src/imgact/test/run_vmsrms_native.sh b/src/imgact/test/run_vmsrms_native.sh index d9579e61f..47ef879d7 100755 --- a/src/imgact/test/run_vmsrms_native.sh +++ b/src/imgact/test/run_vmsrms_native.sh @@ -109,7 +109,7 @@ done $CC -fPIC -mno-outline-atomics -c -o "$WORK/sys_syscall.o" "$LIBVMSSYS_DIR/arch/aarch64/syscall.S" SYSOBJS="$SYSOBJS $WORK/sys_syscall.o" "$WORK/LINK.EXE" --shareable \ - --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE" \ + --symbol-vector "vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE,vms_kif_chkpriv=PROCEDURE,vms_kif_alloc=PROCEDURE,vms_kif_dalloc=PROCEDURE" \ --gsmatch LEQUAL,1,0 -o "$SYSLIB/LIBVMSSYS\$SHR.EXE" $SYSOBJS echo "== mk_vmsprocess_shr.sh: real src/vmsprocess -> LIBVMSPROCESS\$SHR.EXE ==" diff --git a/src/kernel/vms_ioctl.h b/src/kernel/vms_ioctl.h index 966f77626..c7a723e16 100644 --- a/src/kernel/vms_ioctl.h +++ b/src/kernel/vms_ioctl.h @@ -113,6 +113,20 @@ struct vms_mode_args { #define VMS_PRV_V_SETPRV 14 #define VMS_PRV_V_TMPMBX 15 #define VMS_PRV_V_WORLD 16 +#define VMS_PRV_V_MOUNT 17 /* execute the MOUNT ACP function (vms-651). + * Public $PRVDEF documentation bit position + * (VSI OpenVMS System Services Reference + * Manual / Guide to System Security), same + * provenance class as SYSNAM/GRPNAM/GRPPRV + * above -- invariant across VAX/Alpha/ + * Itanium/x86, and the only unclaimed slot + * between oracle-confirmed WORLD=16 and + * OPER=18. src/libvms/include/prvdef.h + * already carried PRV$V_MOUNT=17 from before + * this item; the _Static_assert in + * prv_agreement.c is what makes this copy + * agree with that one, not what re-derives + * either. */ #define VMS_PRV_V_OPER 18 #define VMS_PRV_V_NETMBX 20 #define VMS_PRV_V_SYSPRV 28 @@ -163,6 +177,7 @@ struct vms_mode_args { #define VMS_PRV_M_PRMMBX (1ULL << VMS_PRV_V_PRMMBX) #define VMS_PRV_M_TMPMBX (1ULL << VMS_PRV_V_TMPMBX) #define VMS_PRV_M_WORLD (1ULL << VMS_PRV_V_WORLD) +#define VMS_PRV_M_MOUNT (1ULL << VMS_PRV_V_MOUNT) #define VMS_PRV_M_NETMBX (1ULL << VMS_PRV_V_NETMBX) #define VMS_PRV_M_SYSPRV (1ULL << VMS_PRV_V_SYSPRV) #define VMS_PRV_M_GRPPRV (1ULL << VMS_PRV_V_GRPPRV) @@ -183,6 +198,11 @@ struct vms_mode_args { * LNM$SYSTEM (vms-5b7) * GRPNAM vms_ioctl_lnm_define / vms_ioctl_lnm_delete against * LNM$GROUP (vms-5b7) + * MOUNT cmd_mount / cmd_dismount (src/vmsdcl/dcl_cmd_misc.c) call + * vms_kif_chkpriv(PRV$M_MOUNT) -- itself vms_ioctl_chkpriv, + * i.e. THIS check against proc->cur_privs -- before mount(2)/ + * umount(2)ing a volume (vms-651). Real kernel-enforced state, + * not a userspace getuid() check. * Bits outside this set are STORED and REPORTED (they come from SYSUAF * and VMS reports them) but nothing in this tree gates on them. Adding * a privilege here without adding the check it names is the defect this @@ -210,7 +230,8 @@ struct vms_mode_args { */ #define VMS_PRV_M_ENFORCED (VMS_PRV_M_CMKRNL | VMS_PRV_M_CMEXEC | \ VMS_PRV_M_SETPRV | VMS_PRV_M_WORLD | \ - VMS_PRV_M_SYSNAM | VMS_PRV_M_GRPNAM) + VMS_PRV_M_SYSNAM | VMS_PRV_M_GRPNAM | \ + VMS_PRV_M_MOUNT) struct vms_priv_args { uint64_t mask; /* privilege mask to set/clear/check */ diff --git a/src/libvms/include/ovmx_layout.h b/src/libvms/include/ovmx_layout.h index dff21e4f0..335323ef9 100644 --- a/src/libvms/include/ovmx_layout.h +++ b/src/libvms/include/ovmx_layout.h @@ -146,4 +146,14 @@ #define VMS_USERS_DIR SYSDISK_MOUNT "/USERS" #define VMS_TEMP_DIR SYSDISK_MOUNT "/SYSTMP" +/* + * The setuid-root mount(2)/umount(2) helper MOUNT/DISMOUNT fork+exec + * (tools/vms_mount_helper.c, vms-651). NOT a VMS filespec and never named + * to a user: DCL never shows this path, the same way it never shows the + * console device name it derives internally. Lives outside the VMS tree + * entirely (not under SYSDISK_MOUNT) because it must be reachable even + * when no volume is mounted yet -- it is what MOUNT uses to mount one. + */ +#define VMS_MOUNT_HELPER_PATH "/sbin/vms_mount_helper" + #endif /* __OVMX_LAYOUT_H */ diff --git a/src/libvms/prv_agreement.c b/src/libvms/prv_agreement.c index 558f0b29b..bec6381e3 100644 --- a/src/libvms/prv_agreement.c +++ b/src/libvms/prv_agreement.c @@ -58,6 +58,7 @@ _Static_assert(PRV$V_SETPRI == VMS_PRV_V_SETPRI, "PRV$V_SETPRI disagrees with th _Static_assert(PRV$V_SETPRV == VMS_PRV_V_SETPRV, "PRV$V_SETPRV disagrees with the executive"); _Static_assert(PRV$V_TMPMBX == VMS_PRV_V_TMPMBX, "PRV$V_TMPMBX disagrees with the executive"); _Static_assert(PRV$V_WORLD == VMS_PRV_V_WORLD, "PRV$V_WORLD disagrees with the executive"); +_Static_assert(PRV$V_MOUNT == VMS_PRV_V_MOUNT, "PRV$V_MOUNT disagrees with the executive"); _Static_assert(PRV$V_OPER == VMS_PRV_V_OPER, "PRV$V_OPER disagrees with the executive"); _Static_assert(PRV$V_NETMBX == VMS_PRV_V_NETMBX, "PRV$V_NETMBX disagrees with the executive"); _Static_assert(PRV$V_SYSPRV == VMS_PRV_V_SYSPRV, "PRV$V_SYSPRV disagrees with the executive"); @@ -77,6 +78,7 @@ _Static_assert(PRV$M_GRPNAM == VMS_PRV_M_GRPNAM, "PRV$M_GRPNAM disagrees with th _Static_assert(PRV$M_SETPRV == VMS_PRV_M_SETPRV, "PRV$M_SETPRV disagrees with the executive"); _Static_assert(PRV$M_TMPMBX == VMS_PRV_M_TMPMBX, "PRV$M_TMPMBX disagrees with the executive"); _Static_assert(PRV$M_WORLD == VMS_PRV_M_WORLD, "PRV$M_WORLD disagrees with the executive"); +_Static_assert(PRV$M_MOUNT == VMS_PRV_M_MOUNT, "PRV$M_MOUNT disagrees with the executive"); _Static_assert(PRV$M_NETMBX == VMS_PRV_M_NETMBX, "PRV$M_NETMBX disagrees with the executive"); _Static_assert(PRV$M_SYSPRV == VMS_PRV_M_SYSPRV, "PRV$M_SYSPRV disagrees with the executive"); _Static_assert(PRV$M_GRPPRV == VMS_PRV_M_GRPPRV, "PRV$M_GRPPRV disagrees with the executive"); diff --git a/src/libvmssys/vms_kif.h b/src/libvmssys/vms_kif.h index 0f8159ac2..2dc5b13f0 100644 --- a/src/libvmssys/vms_kif.h +++ b/src/libvmssys/vms_kif.h @@ -121,10 +121,14 @@ uint32_t vms_kif_kerr_to_ss(int err); /* ================================================================ * Access Mode (3a) * - * THE WHOLE FAMILY IS UNWIRED: no product code calls any of it, so - * privileges and access modes are still whatever a process says they are - * (src/vmsdcl/dcl_main.c reads VMS_PRIVILEGES from the environment). - * vms-pv1 is the item that makes the executive the enforcer. + * STALE AS A BLANKET CLAIM (corrected here, not re-derived): setmode/getmode + * remain unwired -- privileges and access modes are still whatever a process + * says they are (src/vmsdcl/dcl_main.c reads VMS_PRIVILEGES from the + * environment) -- but setprv and, as of vms-651, chkpriv are NOT: sys$setprv + * routes real $SETPRV mutations through the executive, and cmd_mount/ + * cmd_dismount ask it whether PRV$M_MOUNT is held before touching a volume. + * vms-pv1 is the item that makes the executive the enforcer for the REST of + * this family. * ================================================================ */ /* Set access mode. Returns SS$_ status @@ -142,8 +146,10 @@ uint32_t vms_kif_getmode(uint8_t *mode, uint64_t *cur_privs, uint64_t *perm_priv uint32_t vms_kif_setprv(uint64_t mask, int enable, int permanent, uint64_t *prev); /* Check if privileges are held. Returns SS$_NORMAL or SS$_NOPRIV - * OVMX-UNWIRED: vms_kif_chkpriv (vms-pv1) -- no privilege check in OVMX asks - * the executive, which is why any process can still claim any privilege */ + * Wired (vms-651): cmd_mount/cmd_dismount (src/vmsdcl/dcl_cmd_misc.c) check + * PRV$M_MOUNT this way before mount(2)/umount(2)ing a volume -- the first + * product path that asks the executive whether a privilege is held, rather + * than trusting a self-declared mask. */ uint32_t vms_kif_chkpriv(uint64_t mask); /* ================================================================ @@ -280,21 +286,25 @@ uint32_t vms_kif_dassgn(uint32_t chan); * 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. - * OVMX-UNWIRED: vms_kif_alloc (vms-dv1) */ + * Wired (vms-651): cmd_mount (src/vmsdcl/dcl_cmd_misc.c) claims the unit in + * the executive's device table before mount(2)ing its backing block device, + * so a second process cannot MOUNT (or ALLOCATE, once vms-dv1 lands) the + * same unit out from under it. ALLOCATE/DEALLOCATE as their own DCL verbs + * remain vms-dv1's to wire; this is a second, independent caller. */ uint32_t vms_kif_alloc(const char *devnam); /* $DALLOC the device. SS$_DEVNOTALLOC if this process does not have it * allocated. - * OVMX-UNWIRED: vms_kif_dalloc (vms-dv1) */ + * Wired (vms-651): cmd_dismount releases the claim vms_kif_alloc() took, + * after umount(2) succeeds. */ uint32_t vms_kif_dalloc(const char *devnam); /* vms_kif_alloc_op() is the static body those two share inside vms_kif.c. * It has no prototype here and is not part of the interface, but the census * universe is the union of what this header prototypes and what vms_kif.c * defines -- static definitions included, so that marking a definition static - * cannot drop it out of the census. It is reached only from $ALLOC/$DALLOC, - * so while they are unwired it is unwired too, and it says so: - * OVMX-UNWIRED: vms_kif_alloc_op (vms-dv1) -- shared body of the two above */ + * cannot drop it out of the census. It is reached from $ALLOC/$DALLOC, both + * wired (vms-651), so it is wired too. */ /* Read a device row by name. SS$_NOSUCHDEV if there is no such device. * Wired: the census gate is what proves it has a product caller. */ @@ -321,9 +331,9 @@ uint32_t vms_kif_devscan(uint32_t *index, struct vms_devinfo *info); * table as DC$_DISK rows); this is the companion that hands back the backing * device the process must open. The executive owns the fact -- the process * never scans /sys/block itself (Rule 11). - * OVMX-UNWIRED: vms_kif_disk_resolve (vms-651) -- MOUNT is the consumer: real - * MOUNT/DISMOUNT (vms-651) resolves a unit to its Linux block device to open - * it. vms-651 is blocked on this item (vms-3e8) and wires it. */ + * Wired (vms-651): cmd_mount resolves the unit to its backing block device + * this way, then mount(2)s "/dev/" as vmsfs at the unit's mount + * point. */ uint32_t vms_kif_disk_resolve(const char *devnam, char *backing, uint32_t backing_size, uint32_t *major, uint32_t *minor); diff --git a/src/ovmx_init/ovmx_init.c b/src/ovmx_init/ovmx_init.c index 10020096f..28f770854 100644 --- a/src/ovmx_init/ovmx_init.c +++ b/src/ovmx_init/ovmx_init.c @@ -50,6 +50,7 @@ #include "ovmx_layout.h" #include "ovmx_identity.h" #include "ssdef.h" +#include "dcdef.h" /* PID 1's identity is established THROUGH the executive, not declared. */ #include "vms_kif.h" @@ -234,6 +235,54 @@ static void ovmx_sysinit_halt(const char *what, const char *detail) halt_now(); } +/* + * Pre-create every disk unit's mount point directory, as root, before any + * VMS session -- which runs de-privileged under its SYSUAF UIC (LOGINOUT + * setuid()/setgid()'s every session onto it, tools/vms_login.c) -- ever + * needs one (vms-651). + * + * WHY THIS HAS TO HAPPEN HERE, not in DCL. mkdir(2) under a directory's + * parent needs write+search there for the CALLING process's uid/gid; a VMS + * session's Linux uid IS its SYSUAF UIC member number, not root (VMS + * privilege is not Linux capability -- see the vms_kif_disk_mount() comment + * in src/kernel/vms_devtab.c for the other half of this), so it cannot + * create a NEW directory under /mnt (root-owned, mode 0755) no matter what + * VMS privilege it holds. PID 1 is the one thing on the node still root at + * this point in boot, so it is the one thing that can create these + * directories -- MOUNT (src/vmsdcl/dcl_cmd_misc.c) only ever targets a path + * that already exists. + * + * The disk units come from the executive's own enumeration + * (vms_kif_devscan(), src/kernel/vms_devtab.c -- vda -> DKA0:, vdb -> + * DKA100:, ...), not a second, independent scan of /dev (Rule 11): PID 1 + * asks the SAME table MOUNT will ask. + */ +static void provision_disk_mount_points(void) +{ + struct vms_devinfo info; + uint32_t index = 0; + + mkdir("/mnt", 0755); + + while (vms_kif_devscan(&index, &info) == SS$_NORMAL) { + if (info.devclass != DC$_DISK) + continue; + + char name[VMS_DEVNAM_SIZE]; + strncpy(name, info.devnam, sizeof(name) - 1); + name[sizeof(name) - 1] = '\0'; + size_t len = strlen(name); + if (len > 0 && name[len - 1] == ':') + name[len - 1] = '\0'; + for (size_t i = 0; name[i]; i++) + name[i] = (char)tolower((unsigned char)name[i]); + + char mount_point[64]; + snprintf(mount_point, sizeof(mount_point), "/mnt/%s", name); + mkdir(mount_point, 0755); + } +} + /* * Load a kernel module. Returns 0 on success, -1 with errno set otherwise. * Callers decide whether a failure is survivable -- for the executive it is @@ -352,6 +401,11 @@ static void bare_metal_init(void) /* The executive comes up before anything else runs. */ executive_attach(); + /* MOUNT (vms-651) needs every disk unit's mount point to already exist + * before any de-privileged VMS session tries to use one -- see the + * function's own comment for why this can only happen here, as root. */ + provision_disk_mount_points(); + /* vmsfs.ko is the filesystem, not the executive; a failure here surfaces * as the mount failure below, which halts honestly. The executive itself * is loaded and pinned by executive_attach(). */ diff --git a/src/vmsdcl/dcl_builtin.c b/src/vmsdcl/dcl_builtin.c index 441fb73d0..c965093a9 100644 --- a/src/vmsdcl/dcl_builtin.c +++ b/src/vmsdcl/dcl_builtin.c @@ -40,42 +40,6 @@ const char *vms_months[] = { "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; -/* ================================================================== */ -/* VMS Device Table */ -/* ================================================================== */ - -struct vms_device vms_device_table[VMS_MAX_DEVICES]; -int vms_device_count = 0; - -/* - * Find a device in the table by VMS name (case-insensitive). - * The name may or may not include trailing colon. - */ -struct vms_device *vms_find_device(const char *name) -{ - char upper[16]; - size_t len = strlen(name); - if (len >= sizeof(upper)) len = sizeof(upper) - 1; - for (size_t i = 0; i < len; i++) - upper[i] = (char)toupper((unsigned char)name[i]); - upper[len] = '\0'; - /* Strip trailing colon for comparison */ - if (len > 0 && upper[len - 1] == ':') - upper[--len] = '\0'; - - for (int i = 0; i < vms_device_count; i++) { - char dev[16]; - strncpy(dev, vms_device_table[i].vms_name, sizeof(dev) - 1); - dev[sizeof(dev) - 1] = '\0'; - size_t dlen = strlen(dev); - if (dlen > 0 && dev[dlen - 1] == ':') - dev[--dlen] = '\0'; - if (strcasecmp(upper, dev) == 0) - return &vms_device_table[i]; - } - return NULL; -} - /* ================================================================== */ /* Command Table */ /* ================================================================== */ diff --git a/src/vmsdcl/dcl_cmd_misc.c b/src/vmsdcl/dcl_cmd_misc.c index fb80dfd23..8b09637b7 100644 --- a/src/vmsdcl/dcl_cmd_misc.c +++ b/src/vmsdcl/dcl_cmd_misc.c @@ -43,7 +43,9 @@ #include "starlet.h" #include "ovmx_identity.h" #include "vmsfs/filespec.h" +#include "vmsfs/device.h" #include "ovmx_layout.h" +#include "vms_kif.h" #ifdef HAVE_READLINE #include @@ -1590,9 +1592,166 @@ int cmd_tcpip(struct dcl_command *cmd) /* ================================================================== */ /* - * MOUNT - Mount a VMS device (virtual mapping to a directory). + * Compute the mount point a device name uses. A PURE FUNCTION of the name -- + * any process can compute it without asking anyone, exactly like + * SYSDISK_MOUNT is a compile-time constant for DKA0: (ovmx_layout.h). This is + * what lets "is this unit mounted" be answered from /proc/mounts (real, + * global, kernel-reported truth) instead of a per-process table entry. + * "/mnt/" matches the vmsfs mount-point convention the QEMU kernel + * tests already use (tests/qemu/test_kmod_vmsfs*.c). + */ +static void mount_point_for_device(const char *log_name, char *buf, size_t sz) +{ + char lower[16]; + size_t i; + for (i = 0; log_name[i] && i < sizeof(lower) - 1; i++) + lower[i] = (char)tolower((unsigned char)log_name[i]); + lower[i] = '\0'; + snprintf(buf, sz, "/mnt/%s", lower); +} + +/* + * Is `mount_point` present in the kernel's own mount table right now? + * Cross-process, kernel-reported truth -- not a field only this process + * could see. + * + * Parsed by hand with fopen/fgets rather than glibc's getmntent(3): + * DECC$SHR's symbol vector (src/vmslink/mk_decc_shr.sh) exports the plain + * stdio family DCL already links against everywhere, but not + * setmntent/getmntent/endmntent -- those are a glibc-only convenience this + * tree has never needed before, and pulling them in here broke the + * VMS-native LINK.EXE build of DCL.EXE (%LINK-F-ERROR, unresolved external + * symbol 'setmntent'; measured building distro/Dockerfile.bootable). + */ +static int mount_point_is_mounted(const char *mount_point) +{ + FILE *fp = fopen("/proc/mounts", "r"); + if (!fp) + return 0; + + /* /proc/mounts format: " + * \n", space-separated. Compare the SECOND field's exact + * extent, not a substring -- "/mnt/dka1" must not match + * "/mnt/dka100". */ + char line[512]; + size_t mp_len = strlen(mount_point); + int found = 0; + while (fgets(line, sizeof(line), fp)) { + char *field2 = strchr(line, ' '); + if (!field2) continue; + field2++; + char *after = strchr(field2, ' '); + if (!after) continue; + size_t flen = (size_t)(after - field2); + if (flen == mp_len && strncmp(field2, mount_point, flen) == 0) { + found = 1; + break; + } + } + fclose(fp); + return found; +} + +/* + * Fork, continue THIS session's VMS identity onto the child, and exec the + * setuid-root mount helper (tools/vms_mount_helper.c, vms-651) -- the one + * thing on the node still able to mount(2)/umount(2) once LOGINOUT has + * setuid()/setgid()'d the session onto its SYSUAF UIC. See that file's + * header comment for the full reasoning (including why a kernel-mediated + * mount does not work on this platform) and for why the helper re-derives + * its own authorization rather than trusting argv. + * + * vms_kif_register_continue() is called in the CHILD, before execv() -- + * the SAME "forked child before doing privileged work inherits the + * parent's authenticated identity" shape image activation already uses + * (its own doc comment in src/libvmssys/vms_kif.h says so explicitly) -- + * so the helper's PRV$M_MOUNT check sees THIS session, not a fresh, + * unauthenticated registration: pid/tgid survive exec(), uid/gid/ + * capabilities do not, and vms.ko's registration is keyed on the former. + * + * Returns the helper's exit code (0 success, 1 mount/umount itself failed, + * 2 usage error, 3 PRV$M_MOUNT not held), or -1 if the helper could not be + * forked/exec'd/waited at all. On exit code 1, *helper_errno receives the + * errno the helper's mount(2)/umount(2) call failed with. + */ +static int run_mount_helper(char *const argv[], int *helper_errno) +{ + if (helper_errno) + *helper_errno = 0; + + int pipefd[2]; + if (pipe(pipefd) != 0) + return -1; + + pid_t pid = fork(); + if (pid < 0) { + close(pipefd[0]); + close(pipefd[1]); + return -1; + } + if (pid == 0) { + close(pipefd[0]); + dup2(pipefd[1], STDERR_FILENO); + close(pipefd[1]); + (void)vms_kif_open(); + (void)vms_kif_register_continue(); + execv(VMS_MOUNT_HELPER_PATH, argv); + _exit(127); + } + + close(pipefd[1]); + char errbuf[256]; + ssize_t n = read(pipefd[0], errbuf, sizeof(errbuf) - 1); + close(pipefd[0]); + errbuf[n > 0 ? n : 0] = '\0'; + + int wstatus; + if (waitpid(pid, &wstatus, 0) < 0 || !WIFEXITED(wstatus)) + return -1; + + int rc = WEXITSTATUS(wstatus); + if (rc == 1 && helper_errno) { + int e = 0; + if (sscanf(errbuf, "ERRNO %d", &e) == 1) + *helper_errno = e; + } + return rc; +} + +/* + * MOUNT - mount(2) a VMS disk unit's backing block device as vmsfs (vms-651). * * Syntax: MOUNT device: label [/SYSTEM] + * + * KILLED (vms-651, docs/design-vms-faithful-install.md sec 3.1/3.3): the + * facade this replaced never called mount(2). It wrote a per-process + * userspace device table (struct vms_device / vms_device_table[] in + * src/vmsdcl/dcl_builtin.c -- deleted with this change, its only two + * readers), used getcwd() as the "mount path", and printed + * %MOUNT-I-MOUNTED unconditionally: success reported, nothing shared with + * any other process or the disk -- the Rule 9 defect class exactly. + * + * WHAT THIS DOES NOW: + * - PRV$M_MOUNT is checked through the executive (vms_kif_chkpriv, which + * asks vms_ioctl_chkpriv to read proc->cur_privs -- kernel-resident + * state, not a userspace getuid() check nothing here could forge). + * - The unit is resolved to its backing Linux block device through the + * executive (vms_kif_disk_resolve, vms-3e8) -- this process never + * scans /sys/block itself (Rule 11). + * - "Already mounted" is answered by the kernel's OWN mount table + * (/proc/mounts), not a struct field only this process could see. + * - The unit is claimed in the executive's device table (vms_kif_alloc, + * wired here for the first time) so a second process cannot mount the + * same unit out from under this one. + * - mount(2) attaches "/dev/" as vmsfs at the unit's mount + * point, and the per-process VMS-filespec translator (vmsfs_device_add, + * src/vmsfs/vmsfs_device.c -- the same mechanism DKA0:/SYSDISK_MOUNT + * already uses) learns the mapping so CREATE/OPEN against this device + * resolve for the rest of this session. + * - A mount(2) failure has no oracle-pinned VMS status (VMS has no + * Linux mount(2) underneath it), so it is reported as an honest OVMX + * facility (Rule 10), the same shape ovmx_sysinit_halt() uses in + * src/ovmx_init/ovmx_init.c for the system disk's own mount failure. */ int cmd_mount(struct dcl_command *cmd) { @@ -1624,65 +1783,130 @@ int cmd_mount(struct dcl_command *cmd) dev_name[nlen + 1] = '\0'; } - /* Check if already mounted */ - struct vms_device *existing = vms_find_device(dev_name); - if (existing && existing->mounted) { + /* Strip trailing colon for the logical name / filespec-translator key */ + char log_name[16]; + strncpy(log_name, dev_name, sizeof(log_name) - 1); + log_name[sizeof(log_name) - 1] = '\0'; + size_t lnlen = strlen(log_name); + if (lnlen > 0 && log_name[lnlen - 1] == ':') + log_name[lnlen - 1] = '\0'; + + char mount_point[64]; + mount_point_for_device(log_name, mount_point, sizeof(mount_point)); + + (void)vms_kif_open(); + + /* PRIVILEGE (vms-651 constraint): ask the executive, not getuid(). */ + uint32_t pst = vms_kif_chkpriv(PRV$M_MOUNT); + if (pst == SS$_NOPRIV) { + dcl_error("SYSTEM", 4, "NOPRIV", + "insufficient privilege or object protection violation"); + return SS$_NOPRIV; + } + if (!(pst & 1)) { + /* Executive-unreachable / ioctl-level failure -- $STATUS carries + * it, nothing rendered (Rule 10; see cmd_show_device's identical + * default: case in dcl_cmd_show.c for the reasoning). */ + return pst; + } + + /* Resolve the unit to its backing block device through the executive + * (vms_kif_disk_resolve, vms-3e8) -- the process never scans /sys/block + * itself (Rule 11). */ + char backing[VMS_BACKING_SIZE]; + memset(backing, 0, sizeof(backing)); + uint32_t rst = vms_kif_disk_resolve(dev_name, backing, sizeof(backing), + NULL, NULL); + switch (rst) { + case SS$_NORMAL: + break; + case SS$_NOSUCHDEV: + dcl_error("SYSTEM", 0, "NOSUCHDEV", "no such device available"); + return SS$_NOSUCHDEV; + case SS$_IVDEVNAM: + dcl_error("SYSTEM", 0, "IVDEVNAM", "invalid device name"); + return SS$_IVDEVNAM; + default: + return rst; + } + + if (mount_point_is_mounted(mount_point)) { dcl_error("MOUNT", 2, "DEVMOUNT", "device already mounted - _%s", dev_name); return SS$_DEVMOUNT; } - /* Get volume label */ + /* Claim the unit in the executive's device table (vms-651 wires + * vms_kif_alloc for the first time). */ + uint32_t ast = vms_kif_alloc(dev_name); + if (ast == SS$_DEVALLOC) { + dcl_error("SYSTEM", 0, "DEVALLOC", + "device already allocated to another user"); + return SS$_DEVALLOC; + } + if (!(ast & 1)) + return ast; + + /* Volume label -- informational only; vmsfs does not read it back. + * Kept for command-line compatibility. */ char label[16] = "OVMX"; if (cmd->param_count >= 2) { size_t llen = strlen(cmd->params[1]); if (llen >= sizeof(label)) llen = sizeof(label) - 1; - for (size_t i = 0; i < llen; i++) - label[i] = (char)toupper((unsigned char)cmd->params[1][i]); + for (size_t k = 0; k < llen; k++) + label[k] = (char)toupper((unsigned char)cmd->params[1][k]); label[llen] = '\0'; } - /* Use current directory as mount path */ - char linux_path[256]; - if (!getcwd(linux_path, sizeof(linux_path))) { - strncpy(linux_path, "/", sizeof(linux_path) - 1); - linux_path[sizeof(linux_path) - 1] = '\0'; - } - - /* Add or update device table entry */ - struct vms_device *dev = existing; - if (!dev) { - if (vms_device_count >= VMS_MAX_DEVICES) { - dcl_error("MOUNT", 2, "DEVFULL", - "device table full"); - return SS$_DEVALLOC; - } - dev = &vms_device_table[vms_device_count++]; - } - strncpy(dev->vms_name, dev_name, sizeof(dev->vms_name) - 1); - dev->vms_name[sizeof(dev->vms_name) - 1] = '\0'; - strncpy(dev->linux_path, linux_path, sizeof(dev->linux_path) - 1); - dev->linux_path[sizeof(dev->linux_path) - 1] = '\0'; - strncpy(dev->volume_label, label, sizeof(dev->volume_label) - 1); - dev->volume_label[sizeof(dev->volume_label) - 1] = '\0'; - dev->mounted = 1; + /* + * The actual mount(2), performed by the setuid-root helper + * (tools/vms_mount_helper.c, vms-651): LOGINOUT setuid()/setgid()'s + * every VMS session onto its SYSUAF UIC (tools/vms_login.c), so THIS + * process holds no Linux capability regardless of the PRV$M_MOUNT it + * was just found to hold -- mount(2) itself requires CAP_SYS_ADMIN + * unconditionally. The helper re-derives PRV$M_MOUNT from the + * executive itself (it does not trust this process's claim) before + * doing anything privileged -- see its header comment for why a + * kernel-mediated mount is not available on this platform and for the + * full security reasoning. The mount point already exists -- + * src/ovmx_init/ovmx_init.c's provision_disk_mount_points() created it, + * as root, before any session dropped privilege. + */ + char backing_path[VMS_BACKING_SIZE + 8]; + snprintf(backing_path, sizeof(backing_path), "/dev/%s", backing); + char *helper_argv[] = { + (char *)VMS_MOUNT_HELPER_PATH, (char *)"mount", + backing_path, mount_point, NULL + }; + int helper_errno = 0; + int hrc = run_mount_helper(helper_argv, &helper_errno); + if (hrc == 3) { + vms_kif_dalloc(dev_name); + dcl_error("SYSTEM", 4, "NOPRIV", + "insufficient privilege or object protection violation"); + return SS$_NOPRIV; + } + if (hrc != 0) { + vms_kif_dalloc(dev_name); + dcl_error("OVMX", 4, "MOUNTFAIL", + "%s would not mount as vmsfs: %s", dev_name, + hrc == 1 ? strerror(helper_errno) : "mount helper did not run"); + return SS$_BUGCHECK; + } + + /* Tell the per-process VMS-filespec translator (the same mechanism + * DKA0:/SYSDISK_MOUNT already uses) so filespecs against this device + * resolve for the rest of this session. */ + vmsfs_device_add(log_name, mount_point); /* Create logical name for device -> linux path */ const char *table = LNM_PROCESS_TABLE; if (dcl_has_qualifier(cmd, "SYSTEM")) table = LNM_SYSTEM_TABLE; - /* Strip trailing colon for logical name */ - char log_name[16]; - strncpy(log_name, dev_name, sizeof(log_name) - 1); - log_name[sizeof(log_name) - 1] = '\0'; - size_t lnlen = strlen(log_name); - if (lnlen > 0 && log_name[lnlen - 1] == ':') - log_name[lnlen - 1] = '\0'; - lnm_manager_t *mgr = lnm_get_manager(); if (mgr) { - lnm_create(mgr, table, log_name, linux_path, + lnm_create(mgr, table, log_name, mount_point, LNM_ATTR_TERMINAL, LNM_MODE_USER); } @@ -1692,9 +1916,13 @@ int cmd_mount(struct dcl_command *cmd) /* - * DISMOUNT - Dismount a VMS device. + * DISMOUNT - umount(2) a VMS disk unit dismounted by MOUNT (vms-651). * * Syntax: DISMOUNT device: + * + * Mirrors cmd_mount(): PRV$M_MOUNT through the executive, "is it mounted" + * from /proc/mounts, umount(2), then release the executive's claim + * (vms_kif_dalloc) and the filespec-translator entry. */ int cmd_dismount(struct dcl_command *cmd) { @@ -1718,16 +1946,6 @@ int cmd_dismount(struct dcl_command *cmd) dev_name[nlen + 1] = '\0'; } - struct vms_device *dev = vms_find_device(dev_name); - if (!dev || !dev->mounted) { - dcl_error("DISMOUNT", 2, "DEVNOTMNT", - "device is not mounted - _%s", dev_name); - return SS$_DEVNOTMOUNT; - } - - dev->mounted = 0; - - /* Remove logical name */ char log_name[16]; strncpy(log_name, dev_name, sizeof(log_name) - 1); log_name[sizeof(log_name) - 1] = '\0'; @@ -1735,6 +1953,49 @@ int cmd_dismount(struct dcl_command *cmd) if (lnlen > 0 && log_name[lnlen - 1] == ':') log_name[lnlen - 1] = '\0'; + char mount_point[64]; + mount_point_for_device(log_name, mount_point, sizeof(mount_point)); + + (void)vms_kif_open(); + + uint32_t pst = vms_kif_chkpriv(PRV$M_MOUNT); + if (pst == SS$_NOPRIV) { + dcl_error("SYSTEM", 4, "NOPRIV", + "insufficient privilege or object protection violation"); + return SS$_NOPRIV; + } + if (!(pst & 1)) + return pst; + + if (!mount_point_is_mounted(mount_point)) { + dcl_error("DISMOUNT", 2, "DEVNOTMNT", + "device is not mounted - _%s", dev_name); + return SS$_DEVNOTMOUNT; + } + + /* The actual umount(2), performed by the setuid-root helper -- see the + * matching comment on the mount(2) call in cmd_mount(). */ + char *helper_argv[] = { + (char *)VMS_MOUNT_HELPER_PATH, (char *)"umount", mount_point, NULL + }; + int helper_errno = 0; + int hrc = run_mount_helper(helper_argv, &helper_errno); + if (hrc == 3) { + dcl_error("SYSTEM", 4, "NOPRIV", + "insufficient privilege or object protection violation"); + return SS$_NOPRIV; + } + if (hrc != 0) { + dcl_error("OVMX", 4, "DISMOUNTFAIL", + "%s would not unmount: %s", dev_name, + hrc == 1 ? strerror(helper_errno) : "mount helper did not run"); + return SS$_BUGCHECK; + } + + vms_kif_dalloc(dev_name); + vmsfs_device_remove(log_name); + + /* Remove logical name */ lnm_manager_t *mgr = lnm_get_manager(); if (mgr) { lnm_delete(mgr, LNM_PROCESS_TABLE, log_name, LNM_MODE_USER); diff --git a/src/vmsdcl/include/dcl/dcl_cmd.h b/src/vmsdcl/include/dcl/dcl_cmd.h index 365b468f7..c06a0e2de 100644 --- a/src/vmsdcl/include/dcl/dcl_cmd.h +++ b/src/vmsdcl/include/dcl/dcl_cmd.h @@ -99,21 +99,6 @@ int cmd_product(struct dcl_command *cmd); /* VMS month abbreviations */ extern const char *vms_months[]; -/* VMS device table */ -#define VMS_MAX_DEVICES 64 - -struct vms_device { - char vms_name[16]; - char linux_path[256]; - char volume_label[16]; - int mounted; -}; - -extern struct vms_device vms_device_table[VMS_MAX_DEVICES]; -extern int vms_device_count; - -struct vms_device *vms_find_device(const char *name); - /* Queue initialization helper */ int ensure_queue_init(void); diff --git a/src/vmslink/mk_dcl.sh b/src/vmslink/mk_dcl.sh index 054cca8d4..892c291b5 100755 --- a/src/vmslink/mk_dcl.sh +++ b/src/vmslink/mk_dcl.sh @@ -25,8 +25,12 @@ # - vmsfs_* filespec/protection/device universals -> LIBVMSFS$SHR; # - the libvms runtime universals (sys$sndopr, str_upcase_copy, # ovmx_accounting_get_lastlogin, vms_terminal_*/vms_term_*, -# vms_severity_char/vms_strerror, AND the DATA tables vms_months / -# vms_device_table / vms_device_count) -> LIBVMS$SHR. +# vms_severity_char/vms_strerror, AND the DATA table vms_months) +# -> LIBVMS$SHR. +# (vms_device_table / vms_device_count, the per-process MOUNT +# facade, were DELETED by vms-651 along with the facade itself -- +# MOUNT/DISMOUNT now go through mount(2)/umount(2) and the +# executive's device table, not a struct in this image.) # IMGACT pulls the full producer graph TRANSITIVELY from DCL's .vms$imp. # (c) is itself a single-TLS-object image: dcl_messages.o defines the __thread # message buffer (10 TLSDESC relocs) — within emit_shareable's supported diff --git a/src/vmslink/mk_vmssys_shr.sh b/src/vmslink/mk_vmssys_shr.sh index b0ca4df3b..63c9b0651 100755 --- a/src/vmslink/mk_vmssys_shr.sh +++ b/src/vmslink/mk_vmssys_shr.sh @@ -72,7 +72,7 @@ echo " cc arch/$ARCH/syscall.S" $CC -fPIC -c -o "$WORK/syscall.o" "$SRC/arch/$ARCH/syscall.S" OBJS="$OBJS $WORK/syscall.o" -SYS_VEC="vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_setident=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_register_continue=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE" +SYS_VEC="vms_strlen=PROCEDURE,vms_kif_open=PROCEDURE,vms_kif_enq=PROCEDURE,vms_kif_deq=PROCEDURE,vms_kif_convert=PROCEDURE,vms_kif_assign=PROCEDURE,vms_kif_dassgn=PROCEDURE,vms_kif_getdvi_chan=PROCEDURE,vms_kif_setprn=PROCEDURE,vms_kif_setprv=PROCEDURE,vms_kif_getjpi_self=PROCEDURE,vms_kif_getjpi_pid=PROCEDURE,vms_kif_getjpi_prcnam=PROCEDURE,vms_kif_procscan=PROCEDURE,vms_kif_setef=PROCEDURE,vms_kif_clref=PROCEDURE,vms_kif_readef=PROCEDURE,vms_kif_waitfr=PROCEDURE,vms_kif_wflor=PROCEDURE,vms_kif_wfland=PROCEDURE,vms_kif_ascefc=PROCEDURE,vms_kif_dacefc=PROCEDURE,vms_kif_dlcefc=PROCEDURE,vms_kif_devscan=PROCEDURE,vms_kif_getdvi_devnam=PROCEDURE,vms_kif_setident=PROCEDURE,vms_kif_dclast=PROCEDURE,vms_kif_setast=PROCEDURE,vms_kif_deliverast=PROCEDURE,vms_kif_lnm_define=PROCEDURE,vms_kif_lnm_delete=PROCEDURE,vms_kif_lnm_translate=PROCEDURE,vms_kif_register_continue=PROCEDURE,vms_kif_mbx_create=PROCEDURE,vms_kif_mbx_assign=PROCEDURE,vms_kif_mbx_delmbx=PROCEDURE,vms_kif_mbx_write=PROCEDURE,vms_kif_mbx_read=PROCEDURE,vms_kif_p0_map=PROCEDURE,vms_kif_p0_unmap=PROCEDURE,vms_kif_p1_map=PROCEDURE,vms_kif_enter_image=PROCEDURE,vms_kif_image_rundown=PROCEDURE,vms_kif_p1_protect=PROCEDURE,vms_kif_lnm_enumerate=PROCEDURE,vms_kif_disk_resolve=PROCEDURE,vms_kif_chkpriv=PROCEDURE,vms_kif_alloc=PROCEDURE,vms_kif_dalloc=PROCEDURE" if [ -n "$EXTRA_VEC" ]; then SYS_VEC="$SYS_VEC,$EXTRA_VEC" fi diff --git a/tests/dcl/test_mount.sh b/tests/dcl/test_mount.sh index 052c082c0..3d458f078 100644 --- a/tests/dcl/test_mount.sh +++ b/tests/dcl/test_mount.sh @@ -1,12 +1,46 @@ #!/bin/bash -# TEST: MOUNT/DISMOUNT commands create and remove VMS device mappings -# EXPECT: contains:%MOUNT-I-MOUNTED, TESTDISK mounted on _DUA0: -# EXPECT: contains:DUA0: -# EXPECT: contains:TESTDISK -# EXPECT: contains:%DISMOUNT-I-DISMOUNTED, _DUA0: dismounted -# EXPECT: contains:%MOUNT-I-MOUNTED, WORK mounted on _DKA0: -# EXPECT: contains:DKA0: -# EXPECT_NOT: contains:%DCL-E- +# TEST: MOUNT/DISMOUNT are real mount(2)/umount(2) plumbing through the +# executive (vms-651) -- honest-failure path, exercised here where +# ctest runs and no /dev/vms exists. +# EXPECT: contains:DCL-ALIVE +# EXPECT: contains:$STATUS = 676 +# EXPECT_NOT: contains:%MOUNT-I-MOUNTED +# EXPECT_NOT: contains:%DISMOUNT-I-DISMOUNTED +# EXPECT_NOT: contains:DUA0: +# EXPECT_NOT: contains:TESTDISK +# +# WHAT USED TO BE HERE, and why it is gone (docs/design-vms-faithful-install.md +# sec 3.1/3.3). cmd_mount never called mount(2): it wrote a per-process +# userspace device table (struct vms_device / vms_device_table[], +# src/vmsdcl/dcl_builtin.c -- deleted with this rewrite), used getcwd() as +# the "mount path", and printed %MOUNT-I-MOUNTED unconditionally. This test +# used to assert THAT unconditional success. It cannot any more: MOUNT now +# resolves the unit through the executive (vms_kif_disk_resolve, vms-3e8), +# checks PRV$M_MOUNT through the executive (vms_kif_chkpriv, not getuid()), +# and only then mount(2)s -- none of which this environment has, so the +# ONLY thing checkable here (CLAUDE.md Rule 9) is that MOUNT fails HONESTLY +# rather than fabricating success. The real mount/dismount/persistence path +# needs a real /dev/vms and a real disk and is proven in QEMU instead -- +# tests/qemu/test_mount_e2e.sh. +# +# THE POSITIVE ANCHOR IS $STATUS, NOT A PRINTED MESSAGE, same reasoning as +# tests/dcl/test_show_device.sh. With no /dev/vms, vms_kif_open() fails and +# every ioctl this process issues fails too (EBADF), which +# src/libvmssys/vms_kif.c's vms_kif_kerr_to_ss() maps to SS$_BUGCHECK (676) +# by default. cmd_mount's FIRST executive call is vms_kif_chkpriv(PRV$M_MOUNT) +# -- before it ever tries to resolve the unit or touch a mount point -- so +# that is where the failure happens, and it is silent (Rule 10: an +# ioctl-level failure is not "the executive did not answer", the same +# unreachable-in-product condition cmd_show_device's default: case +# documents; nothing is rendered, $STATUS carries it). +# +# `WRITE SYS$OUTPUT "DCL-ALIVE"` is the liveness anchor: it never touches the +# executive, so it always leaves $STATUS = 1. A later $STATUS = 676 can only +# be MOUNT's. +# +# THE NEGATIVE ANCHORS keep the FACADE'S OWN VOCABULARY out for good: DUA0: +# and TESTDISK never appear anywhere in this transcript now that MOUNT does +# not fabricate a row for them, and %MOUNT-I-MOUNTED / %DISMOUNT-I-DISMOUNTED +# are the exact facade success lines this item deleted. VMSDCL="${VMSDCL:-vmsdcl}" -export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" -printf 'MOUNT DUA0: TESTDISK\nSHOW DEVICE\nDISMOUNT DUA0:\nMOUNT DKA0: WORK\nSHOW DEVICE\n' | $VMSDCL 2>&1 +printf 'WRITE SYS$OUTPUT "DCL-ALIVE"\nMOUNT DKA100: WORK\nSHOW SYMBOL $STATUS\nDISMOUNT DKA100:\nSHOW SYMBOL $STATUS\n' | $VMSDCL 2>&1 diff --git a/tests/dcl/test_show_device.sh b/tests/dcl/test_show_device.sh index b42df62ba..3ba62961d 100755 --- a/tests/dcl/test_show_device.sh +++ b/tests/dcl/test_show_device.sh @@ -1,6 +1,6 @@ #!/bin/bash # TEST: SHOW DEVICE prints no device row it did not read from the executive -# EXPECT: contains:%MOUNT-I-MOUNTED, TESTDISK mounted on _DUA0: +# EXPECT: contains:DCL-ALIVE # EXPECT: contains:$STATUS = 676 # EXPECT_NOT: regex:^Device +Device +Error # EXPECT_NOT: contains:NOSUCHDEV @@ -10,6 +10,7 @@ # EXPECT_NOT: regex:^[A-Z0-9$_]+: +(Mounted|Dismounted) # EXPECT_NOT: contains:%OVMX-F-EXECDEV # EXPECT_NOT: contains:%OVMX-F-NODEVTAB +# EXPECT_NOT: contains:%MOUNT-I-MOUNTED # # WHAT THIS ASSERTS, and what it deliberately does not (vms-fb9). # @@ -51,6 +52,9 @@ # reasoned, in tests/qemu/test_syssvc_showdev.c and # tests/uat/vms_session_qemu.sh, which both see OPA0: on # every run against a real /dev/vms +# %MOUNT-I-MOUNTED MOUNT's own facade success message, deleted product-wide +# by vms-651 (real mount(2), never a per-process fake) -- +# kept here as history the same way the others are. # # NOSUCHDEV is forbidden here for a different reason -- Rule 10, not row # fabrication. The oracle measured "%SYSTEM-W-NOSUCHDEV, no such device @@ -59,35 +63,32 @@ # all, so printing that message would be a false statement in VMS's own # voice. # -# THE POSITIVE ANCHOR IS NOW $STATUS, NOT A PRINTED MESSAGE (vms-fb9 r5). -# With no /dev/vms, vms_kif_open() fails and every subsequent ioctl this -# process issues fails too (EBADF on the resulting negative descriptor), -# which src/libvmssys/vms_kif.c's vms_kif_kerr_to_ss() maps -- through its -# closed, oracle-pinned errno set -- to SS$_BUGCHECK (676) by default. SHOW -# DEVICE no longer renders that to the user (rule 10: "the executive did not -# answer" is not a user-facing condition), but it still sets $STATUS, and -# SHOW SYMBOL $STATUS reads it back. This is what stops the test passing -# vacuously because SHOW DEVICE prints nothing whatsoever OR because DCL -# never reached it: MOUNT alone leaves $STATUS = 1 (measured, see the MOUNT- -# only case below), so 676 can ONLY appear if SHOW DEVICE itself ran the -# devscan/getdvi call and hit the no-executive ioctl failure. Measured by -# running `printf 'MOUNT DUA0: TESTDISK\nSHOW SYMBOL $STATUS\n' | DCL.EXE` -# in isolation: $STATUS = 1, not 676, confirming MOUNT's own status is not -# what this anchor is keying on. +# THE POSITIVE ANCHOR IS $STATUS, NOT A PRINTED MESSAGE (vms-fb9 r5). With no +# /dev/vms, vms_kif_open() fails and every subsequent ioctl this process +# issues fails too (EBADF on the resulting negative descriptor), which +# src/libvmssys/vms_kif.c's vms_kif_kerr_to_ss() maps -- through its closed, +# oracle-pinned errno set -- to SS$_BUGCHECK (676) by default. SHOW DEVICE no +# longer renders that to the user (rule 10: "the executive did not answer" is +# not a user-facing condition), but it still sets $STATUS, and SHOW SYMBOL +# $STATUS reads it back. # -# MOUNT DUA0: runs FIRST on purpose. It populates that process-local table, -# so SHOW DEVICE in the same process would print DUA0: from it if it still -# had that source. The paired EXPECT on MOUNT's own message is what stops -# this test passing vacuously because DCL never started. +# THE LIVENESS ANCHOR CHANGED FROM MOUNT TO A BARE WRITE (vms-651). Before +# vms-651, MOUNT DUA0: TESTDISK succeeded even with no executive (the +# facade), leaving $STATUS = 1 -- proof DCL was alive and running commands +# BEFORE SHOW DEVICE, so a later $STATUS = 676 could only have come from +# SHOW DEVICE itself. vms-651 deleted that facade: MOUNT now asks the +# executive too (vms_kif_chkpriv before anything else), so with no /dev/vms +# it ALSO fails via the same ioctl path and ALSO leaves $STATUS = 676 -- +# using it as the liveness anchor would make this test pass vacuously +# whether or not SHOW DEVICE itself touched the executive. `WRITE SYS$OUTPUT` +# never asks the executive at all, so it is now the anchor: EXPECT +# "DCL-ALIVE" proves the session ran a command, and it leaves $STATUS = 1, +# so a later $STATUS = 676 can only be SHOW DEVICE's. # # ANCHORING (this repo has shipped assertions satisfiable by something other # than the behaviour under test, so it is spelled out): the EXPECT_NOT -# patterns are line-anchored or contain column runs precisely because MOUNT's -# own success message contains the strings "DUA0:" and "TESTDISK". An -# unanchored `EXPECT_NOT: contains:DUA0:` would be tripped by MOUNT's message -# no matter what SHOW DEVICE did; an unanchored `contains:Mounted` likewise, -# by "mounted on". The old fabricated rows started at column 0 with the -# device name; MOUNT's messages start with "%MOUNT-". +# patterns are line-anchored or contain column runs precisely because the +# old fabricated rows started at column 0 with the device name. # # WHAT IS NOT ASSERTED HERE: that SHOW DEVICE shows OPA0: when an executive # IS present. That needs a real /dev/vms and cannot run under ctest at all @@ -96,4 +97,4 @@ # executive, including the A-writes/B-reads case where another process # allocates the console and SHOW DEVICE observes it. VMSDCL="${VMSDCL:-vmsdcl}" -printf 'MOUNT DUA0: TESTDISK\nSHOW DEVICE\nSHOW SYMBOL $STATUS\nSHOW DEVICE DUA0:\n' | $VMSDCL 2>&1 +printf 'WRITE SYS$OUTPUT "DCL-ALIVE"\nSHOW DEVICE\nSHOW SYMBOL $STATUS\nSHOW DEVICE DUA0:\n' | $VMSDCL 2>&1 diff --git a/tests/qemu/CMakeLists.txt b/tests/qemu/CMakeLists.txt index 4b7b897f1..07441da2e 100644 --- a/tests/qemu/CMakeLists.txt +++ b/tests/qemu/CMakeLists.txt @@ -396,3 +396,23 @@ set_tests_properties(sysgen_versioning_e2e PROPERTIES LABELS "slow;qemu-full-boot" SKIP_RETURN_CODE 77 TIMEOUT 600) + +# --------------------------------------------------------------------------- +# MOUNT/DISMOUNT ARE REAL mount(2)/umount(2) (vms-651). +# +# Boots the ACTUAL mastered bootable image TWICE against a SECOND, real +# virtio disk (formatted VMSFS on the host first -- see run_mount_e2e.sh): +# MOUNT DKA100:, CREATE a real file on it, DISMOUNT, re-MOUNT, then a full +# QEMU process restart against the SAME disk file, proving the file landed +# on the disk and not a tmpfs. See tests/qemu/test_mount_e2e.sh's header for +# the full ground-source shape. Same opt-in convention as parts_demo_e2e +# above, for the same reason (a real docker+QEMU boot, real minutes, must +# not tax a bare `ctest` run's time budget). +add_test( + NAME mount_e2e + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/run_mount_e2e.sh +) +set_tests_properties(mount_e2e PROPERTIES + LABELS "slow;qemu-full-boot" + SKIP_RETURN_CODE 77 + TIMEOUT 900) diff --git a/tests/qemu/run_mount_e2e.sh b/tests/qemu/run_mount_e2e.sh new file mode 100755 index 000000000..11d7484e8 --- /dev/null +++ b/tests/qemu/run_mount_e2e.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# run_mount_e2e.sh - ctest entry point for mount_e2e (vms-651). +# +# THE GATE ITSELF (tests/qemu/test_mount_e2e.sh) drives a REAL docker + QEMU +# boot of the mastered bootable image, twice, against a SECOND, real virtio +# disk it formats on the HOST first. Same convention as +# tests/qemu/run_parts_demo_e2e.sh (read that file's header for the reasoning +# this one shares in full): registered with ctest so it is discoverable by +# name, but gated behind an explicit opt-in so a bare `ctest` run never pays +# for a real docker+QEMU boot it cannot use. +# +# THE ONE THING THIS SCRIPT DOES THAT run_parts_demo_e2e.sh DOES NOT: it +# formats the second disk BEFORE `docker run`, on the HOST, with the host's +# own INITIALIZE.EXE (the userspace mkfs, tools/vms_initialize.c) -- because +# the final "ovmx-boot" runner image (distro/Dockerfile.bootable's `runner` +# stage) ships only qemu + boot artifacts, not the build's host tools. A +# blank, unformatted disk fails to mount(2) as vmsfs (same rule STARTUP.EXE's +# own SYSDISK mount enforces), so the second disk has to be VMSFS-formatted +# before QEMU ever sees it, and that formatting has to happen outside the +# runner image. The formatted image is bind-mounted into the container at +# /work so test_mount_e2e.sh (running inside) can attach it as the guest's +# second virtio disk without needing any host tool of its own. +# +# Env knobs: +# OVMX_QEMU_FULL_E2E must be "1" or this script SKIPs (exit 77). +# OVMX_BOOT_IMAGE image tag to run (default: ovmx-boot). +# OVMX_BUILD_BOOT_IMAGE "1" to build OVMX_BOOT_IMAGE if it is not already +# present (default: 0 - require a prebuilt image). +# OVMX_INITIALIZE_EXE path to a host-built INITIALIZE.EXE (default: +# searches ../../build*/bin/INITIALIZE.EXE relative +# to this script). +# BOOT_TIMEOUT / RUN_TIMEOUT forwarded to the gate itself. + +set -uo pipefail + +SKIP=77 +REPO_ROOT=$(cd "$(dirname "$0")/../.." && pwd) +IMAGE="${OVMX_BOOT_IMAGE:-ovmx-boot}" + +if [ "${OVMX_QEMU_FULL_E2E:-0}" != "1" ]; then + echo "SKIP: mount_e2e requires OVMX_QEMU_FULL_E2E=1 (real docker+QEMU boot, real minutes)." + echo " Run directly: OVMX_QEMU_FULL_E2E=1 OVMX_BOOT_IMAGE=$IMAGE tests/qemu/run_mount_e2e.sh" + exit "$SKIP" +fi + +command -v docker >/dev/null 2>&1 || { echo "SKIP: docker not available"; exit "$SKIP"; } + +if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then + if [ "${OVMX_BUILD_BOOT_IMAGE:-0}" = "1" ]; then + echo "--- building bootable image ($IMAGE) ---" + docker build -t "$IMAGE" -f "$REPO_ROOT/distro/Dockerfile.bootable" "$REPO_ROOT" \ + || { echo "FAIL: bootable image build"; exit 1; } + else + echo "FATAL: image '$IMAGE' not found. Either:" + echo " docker build -t $IMAGE -f distro/Dockerfile.bootable ." + echo "or re-run with OVMX_BUILD_BOOT_IMAGE=1 to build it here." + exit 1 + fi +fi + +# Locate a host-built INITIALIZE.EXE to format the second disk with. +INIT_EXE="${OVMX_INITIALIZE_EXE:-}" +if [ -z "$INIT_EXE" ]; then + for candidate in "$REPO_ROOT"/build/bin/INITIALIZE.EXE "$REPO_ROOT"/build-static/bin/INITIALIZE.EXE; do + if [ -x "$candidate" ]; then INIT_EXE="$candidate"; break; fi + done +fi +if [ -z "$INIT_EXE" ] || [ ! -x "$INIT_EXE" ]; then + echo "SKIP: no host-built INITIALIZE.EXE found (build one: cmake -B build -DBUILD_TOOLS=ON && cmake --build build)." + echo " Set OVMX_INITIALIZE_EXE=/path/to/INITIALIZE.EXE to point at one directly." + exit "$SKIP" +fi + +WORKDIR=$(mktemp -d) +trap 'rm -rf "$WORKDIR"' EXIT + +echo "--- formatting the second disk (DKA100:, label WORK) with $INIT_EXE ---" +"$INIT_EXE" "$WORKDIR/dka100.img" WORK 16 || { echo "FAIL: INITIALIZE of the second disk"; exit 1; } + +exec docker run --rm \ + -e "BOOT_TIMEOUT=${BOOT_TIMEOUT:-90}" \ + -e "RUN_TIMEOUT=${RUN_TIMEOUT:-60}" \ + -v "$REPO_ROOT/tests/qemu/test_mount_e2e.sh:/test.sh:ro" \ + -v "$WORKDIR:/work" \ + --entrypoint bash \ + "$IMAGE" \ + /test.sh diff --git a/tests/qemu/test_mount_e2e.sh b/tests/qemu/test_mount_e2e.sh new file mode 100755 index 000000000..56ac4026b --- /dev/null +++ b/tests/qemu/test_mount_e2e.sh @@ -0,0 +1,332 @@ +#!/bin/bash +# test_mount_e2e.sh - MOUNT/DISMOUNT are real mount(2)/umount(2), ground- +# sourced against a real vms.ko + vmsfs.ko (vms-651, docs/design-vms- +# faithful-install.md sec 3.1/3.3). +# +# WHAT THIS PROVES, AND WHY NOTHING EARLIER PROVES IT. The facade this item +# deleted (src/vmsdcl/dcl_cmd_misc.c's old cmd_mount/cmd_dismount) never +# called mount(2)/umount(2) at all -- it wrote a per-process userspace +# device table, used getcwd() as the "mount path", and printed +# %MOUNT-I-MOUNTED unconditionally. tests/dcl/test_mount.sh (ctest, no +# executive) can only prove the facade is GONE -- an honest failure with no +# /dev/vms is the one thing checkable there. This is the paired POSITIVE: +# a real vms.ko, a real SECOND virtio disk, a real vmsfs mount, a real file +# on it that survives DISMOUNT/re-MOUNT AND a full QEMU process restart -- +# proof it hit the disk, not a tmpfs or a per-process fake sharing nothing +# (CLAUDE.md Rule 9 / INV-6). +# +# THE SECOND DISK is formatted VMSFS on the HOST before this script ever +# runs (run_mount_e2e.sh, with the host's own INITIALIZE.EXE) and handed in +# at /work/dka100.img -- a blank, unformatted disk fails to mount(2) as +# vmsfs, the same rule STARTUP.EXE's own system-disk mount enforces. The +# executive enumerates it as DKA100: (the second virtio-blk device, vms-3e8: +# DKA0: from vda, DKA100: from vdb). +# +# GROUND-SOURCE SHAPE (all of it against the REAL system, no mocks): +# 1. MOUNT DKA100:, CREATE a file on it via DCL (a real COPY, not a +# fabricated row), DISMOUNT, re-MOUNT -> file still there. +# 2. /proc/mounts (via `$ TYPE "/proc/mounts"` -- dcl_resolve_path treats +# a leading "/" as a literal Linux path, so this is a real read of the +# kernel's own mount table, not a VMS filespec; quoted because DCL's +# parser reads a bare leading "/" as a qualifier, not a parameter) +# shows the vmsfs mount +# while mounted, and does NOT after DISMOUNT. +# 3. QEMU is KILLED and a FRESH qemu-system-x86_64 process is started +# against the SAME disk FILE -- proof this hit the disk, not guest +# RAM/tmpfs. The file is still there after the restart. +# 4. NEGATIVE CONTROL: MOUNT of a unit that does not exist (only two +# virtio disks are attached, so DKA200: names no unit) fails with a +# real error and never prints %MOUNT-I-MOUNTED. +# +# Usage (run INSIDE the bootable image, like test_parts_demo_e2e.sh): +# docker build -f distro/Dockerfile.bootable -t ovmx-boot . +# docker run --rm -v $PWD/tests/qemu/test_mount_e2e.sh:/test.sh:ro \ +# -v /path/to/formatted/dka100/dir:/work \ +# --entrypoint bash ovmx-boot /test.sh +# (see tests/qemu/run_mount_e2e.sh, which does the host-side formatting and +# invokes this the same way.) +# +# Env knobs: +# BOOT_TIMEOUT seconds to wait for each boot to reach Username: (default +# 90 -- the distribution disk is pre-installed, so this is +# NOT the blank-disk install budget test_parts_demo_e2e.sh +# needs). +# RUN_TIMEOUT seconds to wait for each DCL command's expected output +# (default 60). +# +# Exit 0 = every ground-source assertion above passed. Exit 1 = a real +# failure (see the printed transcript segment). + +set -uo pipefail + +BOOT_TIMEOUT="${BOOT_TIMEOUT:-90}" +RUN_TIMEOUT="${RUN_TIMEOUT:-60}" +KERNEL=/boot/vmlinuz +INITRD=/boot/initramfs-ovmx.cpio.gz +DISTRIB_IMG=/boot/ovmx-distrib.img +DKA100_SRC=/work/dka100.img +ARCH=$(uname -m) + +if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + QEMU=qemu-system-aarch64 + MACHINE="-machine virt -cpu cortex-a57" + CONSOLE="console=ttyAMA0" +else + QEMU=qemu-system-x86_64 + MACHINE="" + CONSOLE="console=ttyS0" +fi + +for f in "$KERNEL" "$INITRD" "$DISTRIB_IMG" "$DKA100_SRC"; do + [ -f "$f" ] || { echo "FATAL: $f not found - run this inside the ovmx-boot image with /work bind-mounted (see header)"; exit 1; } +done +command -v "$QEMU" >/dev/null 2>&1 || { echo "FATAL: $QEMU not available"; exit 1; } + +PASS=0 +FAIL=0 +ok() { echo " PASS: $1"; PASS=$((PASS + 1)); } +bad() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } + +echo "=== MOUNT/DISMOUNT e2e: real mount(2)/umount(2) through the executive (vms-651) ===" +echo "arch=$ARCH qemu=$QEMU kernel=$KERNEL initrd=$INITRD" + +WORKDIR=$(mktemp -d) +DISK0="$WORKDIR/dka0.img" +DISK1="$WORKDIR/dka100.img" +cp "$DISTRIB_IMG" "$DISK0" +cp "$DKA100_SRC" "$DISK1" + +QPID="" +FIFO="" +LOG="" +cleanup() { [ -n "$QPID" ] && kill "$QPID" 2>/dev/null; rm -rf "$WORKDIR"; } +trap cleanup EXIT + +boot_qemu() { # boot_qemu + local log="$1" fifo="$2" + rm -f "$log" "$fifo" + mkfifo "$fifo" + # shellcheck disable=SC2086 + timeout "$((BOOT_TIMEOUT + RUN_TIMEOUT * 12 + 60))" $QEMU $MACHINE \ + -kernel "$KERNEL" -initrd "$INITRD" \ + -nographic -append "$CONSOLE loglevel=3 quiet" \ + -m 512M -smp 1 -nic none -nodefaults -serial stdio \ + -drive file="$DISK0",format=raw,if=virtio,cache=writethrough \ + -drive file="$DISK1",format=raw,if=virtio,cache=writethrough \ + -no-reboot <"$fifo" >"$log" 2>&1 & + QPID=$! + exec 4>"$fifo" +} + +send() { printf '%s\r' "$1" >&4; } +wait_for() { # pattern limit-seconds since-byte log-file + local pat="$1" limit="${2:-30}" since="${3:-0}" log="${4:-$LOG}" waited=0 + while [ "$waited" -lt "$((limit * 4))" ]; do + if tail -c "+$((since + 1))" "$log" 2>/dev/null | grep -qF "$pat"; then return 0; fi + kill -0 "$QPID" 2>/dev/null || return 1 + sleep 0.25; waited=$((waited + 1)) + done + return 1 +} +segment_since() { tail -c "+$(($1 + 1))" "${2:-$LOG}" 2>/dev/null | tr -d '\r'; } +dump_and_die() { + echo "" + echo "=== FATAL: $1 ===" + echo "--- full console log ($LOG) ---" + cat "$LOG" + kill "$QPID" 2>/dev/null; wait "$QPID" 2>/dev/null; QPID="" + exit 1 +} + +login() { # login -- logs in as SYSTEM, returns once at the $ prompt + local log="$1" + if wait_for 'Username:' "$BOOT_TIMEOUT" 0 "$log"; then + ok "boot reaches the login prompt ($log)" + else + dump_and_die "boot never reached Username: within ${BOOT_TIMEOUT}s" + fi + local off; off=$(wc -c <"$log") + send 'SYSTEM' + wait_for 'Password:' 20 "$off" "$log" && send 'MANAGER' + if wait_for 'Welcome to OVMX' 20 "$off" "$log"; then + ok "SYSTEM logs in" + else + dump_and_die "SYSTEM login failed" + fi + wait_for '$' 20 "$off" "$log" +} + +# ===================================================================== +# BOOT 1 -- mount, create, dismount, re-mount, and the negative control +# ===================================================================== +LOG="$WORKDIR/boot1.log" +boot_qemu "$LOG" "$WORKDIR/boot1.in" +login "$LOG" + +# --- 1. MOUNT DKA100: ------------------------------------------------- +OFF=$(wc -c <"$LOG") +send 'MOUNT DKA100: WORK' +if wait_for '%MOUNT-I-MOUNTED' "$RUN_TIMEOUT" "$OFF"; then + SEG=$(segment_since "$OFF") + if printf '%s\n' "$SEG" | grep -qF '%MOUNT-I-MOUNTED, WORK mounted on _DKA100:'; then + ok "MOUNT DKA100: reports mounted (real mount(2) via the executive)" + else + bad "MOUNT DKA100: printed an unexpected message: $SEG" + fi +else + dump_and_die "MOUNT DKA100: did not reach %MOUNT-I-MOUNTED within ${RUN_TIMEOUT}s" +fi + +# --- 2. /proc/mounts shows the vmsfs mount while mounted --------------- +OFF=$(wc -c <"$LOG") +send 'TYPE "/proc/mounts"' +wait_for '$' "$RUN_TIMEOUT" "$OFF" +SEG=$(segment_since "$OFF") +if printf '%s\n' "$SEG" | grep -qE '/mnt/dka100 +vmsfs'; then + ok "/proc/mounts shows the vmsfs mount at /mnt/dka100 while mounted" +else + bad "/proc/mounts does not show the vmsfs mount while mounted" + echo "$SEG" +fi + +# --- 3. CREATE a real file on it (COPY, not fabricated) ----------------- +OFF=$(wc -c <"$LOG") +send 'COPY SYS$MANAGER:SYSTARTUP_VMS.COM DKA100:[000000]MOUNTTST.TXT' +wait_for '$' "$RUN_TIMEOUT" "$OFF" +SEG=$(segment_since "$OFF") +if printf '%s\n' "$SEG" | grep -qiE '%COPY-[EF]-|%RMS-[EF]-|%DCL-[EF]-'; then + dump_and_die "COPY to DKA100: reported an error: $SEG" +else + ok "COPY to DKA100:[000000]MOUNTTST.TXT reported no error" +fi + +OFF=$(wc -c <"$LOG") +send 'DIRECTORY DKA100:[000000]MOUNTTST.TXT' +wait_for 'Total of' "$RUN_TIMEOUT" "$OFF" +DIR_CMD='DIRECTORY DKA100:[000000]MOUNTTST.TXT' +SEG=$(segment_since "$OFF" | grep -vF "$DIR_CMD") +if printf '%s\n' "$SEG" | grep -qE 'Total of [1-9][0-9]* files?, [0-9]+ blocks' \ + && printf '%s\n' "$SEG" | grep -qF 'MOUNTTST.TXT'; then + ok "DIRECTORY independently confirms MOUNTTST.TXT exists on DKA100:" +else + bad "DIRECTORY does not confirm MOUNTTST.TXT exists on DKA100:" + echo "$SEG" +fi + +OFF=$(wc -c <"$LOG") +send 'TYPE DKA100:[000000]MOUNTTST.TXT' +wait_for '$' "$RUN_TIMEOUT" "$OFF" +TYPE_CMD='TYPE DKA100:[000000]MOUNTTST.TXT' +CONTENT_BEFORE=$(segment_since "$OFF" | grep -vF "$TYPE_CMD") +if [ -n "$(printf '%s' "$CONTENT_BEFORE" | tr -d '[:space:]')" ]; then + ok "TYPE reads real content back from DKA100:" +else + bad "TYPE read nothing back from DKA100: (expected SYSTARTUP_VMS.COM's content)" +fi + +# --- 4. DISMOUNT, and /proc/mounts stops showing it --------------------- +OFF=$(wc -c <"$LOG") +send 'DISMOUNT DKA100:' +if wait_for '%DISMOUNT-I-DISMOUNTED, _DKA100: dismounted' "$RUN_TIMEOUT" "$OFF"; then + ok "DISMOUNT DKA100: reports dismounted (real umount(2))" +else + dump_and_die "DISMOUNT DKA100: did not report success within ${RUN_TIMEOUT}s" +fi + +OFF=$(wc -c <"$LOG") +send 'TYPE "/proc/mounts"' +wait_for '$' "$RUN_TIMEOUT" "$OFF" +SEG=$(segment_since "$OFF") +if printf '%s\n' "$SEG" | grep -qE '/mnt/dka100 +vmsfs'; then + bad "/proc/mounts still shows /mnt/dka100 after DISMOUNT" + echo "$SEG" +else + ok "/proc/mounts no longer shows /mnt/dka100 after DISMOUNT" +fi + +# --- 5. re-MOUNT: the file is still there ------------------------------- +OFF=$(wc -c <"$LOG") +send 'MOUNT DKA100: WORK' +if wait_for '%MOUNT-I-MOUNTED, WORK mounted on _DKA100:' "$RUN_TIMEOUT" "$OFF"; then + ok "re-MOUNT DKA100: succeeds" +else + dump_and_die "re-MOUNT DKA100: did not report success within ${RUN_TIMEOUT}s" +fi + +OFF=$(wc -c <"$LOG") +send 'TYPE DKA100:[000000]MOUNTTST.TXT' +wait_for '$' "$RUN_TIMEOUT" "$OFF" +CONTENT_REMOUNT=$(segment_since "$OFF" | grep -vF "$TYPE_CMD") +if [ "$(printf '%s' "$CONTENT_REMOUNT" | tr -d '[:space:]')" = "$(printf '%s' "$CONTENT_BEFORE" | tr -d '[:space:]')" ] \ + && [ -n "$(printf '%s' "$CONTENT_REMOUNT" | tr -d '[:space:]')" ]; then + ok "the file survives DISMOUNT/re-MOUNT within the same boot, byte-identical" +else + bad "the file did not survive DISMOUNT/re-MOUNT identically" +fi + +# --- 6. NEGATIVE CONTROL: a unit that does not exist --------------------- +OFF=$(wc -c <"$LOG") +send 'MOUNT DKA200: NOPE' +wait_for '$' "$RUN_TIMEOUT" "$OFF" +SEG=$(segment_since "$OFF") +if printf '%s\n' "$SEG" | grep -qF '%MOUNT-I-MOUNTED'; then + bad "MOUNT DKA200: (no such unit) printed %MOUNT-I-MOUNTED anyway" +else + ok "MOUNT DKA200: (no such unit, only two virtio disks attached) prints no %MOUNT-I-MOUNTED" +fi +if printf '%s\n' "$SEG" | grep -qiE '%SYSTEM-[WF]-NOSUCHDEV|%SYSTEM-[WF]-IVDEVNAM'; then + ok "MOUNT DKA200: fails with a real error (no such device)" +else + bad "MOUNT DKA200: did not report a real error" + echo "$SEG" +fi + +# DISMOUNT before killing QEMU so umount(2) flushes the volume cleanly -- +# the same reason test_docker_persistent_disk.sh's fix exists (a killed +# guest can lose dirty page-cache writes that were never fsynced/unmounted). +OFF=$(wc -c <"$LOG") +send 'DISMOUNT DKA100:' +wait_for '%DISMOUNT-I-DISMOUNTED' "$RUN_TIMEOUT" "$OFF" + +kill "$QPID" 2>/dev/null; wait "$QPID" 2>/dev/null; QPID="" + +# ===================================================================== +# BOOT 2 -- a FRESH qemu-system process, SAME disk files: real persistence +# ===================================================================== +LOG="$WORKDIR/boot2.log" +boot_qemu "$LOG" "$WORKDIR/boot2.in" +login "$LOG" + +OFF=$(wc -c <"$LOG") +send 'MOUNT DKA100: WORK' +if wait_for '%MOUNT-I-MOUNTED, WORK mounted on _DKA100:' "$RUN_TIMEOUT" "$OFF"; then + ok "MOUNT DKA100: succeeds again after a full QEMU restart" +else + dump_and_die "MOUNT DKA100: did not report success on the restarted boot" +fi + +OFF=$(wc -c <"$LOG") +send 'TYPE DKA100:[000000]MOUNTTST.TXT' +wait_for '$' "$RUN_TIMEOUT" "$OFF" +CONTENT_AFTER_RESTART=$(segment_since "$OFF" | grep -vF "$TYPE_CMD") +if [ "$(printf '%s' "$CONTENT_AFTER_RESTART" | tr -d '[:space:]')" = "$(printf '%s' "$CONTENT_BEFORE" | tr -d '[:space:]')" ] \ + && [ -n "$(printf '%s' "$CONTENT_AFTER_RESTART" | tr -d '[:space:]')" ]; then + ok "MOUNTTST.TXT survives a full QEMU restart, byte-identical -- proves the disk, not a tmpfs" +else + bad "MOUNTTST.TXT did not survive the QEMU restart identically" +fi + +kill "$QPID" 2>/dev/null; wait "$QPID" 2>/dev/null; QPID="" + +echo "" +echo "RESULT: $PASS passed, $FAIL failed" +if [ "$FAIL" -eq 0 ]; then + echo "ALL MOUNT/DISMOUNT E2E CHECKS PASSED" + exit 0 +fi +echo "" +echo "--- full console log (boot1) ---" +cat "$WORKDIR/boot1.log" 2>/dev/null +echo "--- full console log (boot2) ---" +cat "$WORKDIR/boot2.log" 2>/dev/null +exit 1 diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 44f3b4f36..8f180b49c 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -320,3 +320,35 @@ target_link_libraries(ovmx_kit_pack PRIVATE vmsfs vmslnm) set_target_properties(ovmx_kit_pack PROPERTIES OUTPUT_NAME "ovmx_kit_pack" ) + +# vms_mount_helper - setuid-root privileged mount(2)/umount(2) helper for +# DCL's MOUNT/DISMOUNT (vms-651). See tools/vms_mount_helper.c's header for +# why this exists: LOGINOUT drops every VMS session to its SYSUAF UIC (no +# Linux capability survives that), and there is no exported kernel-module +# API that can graft a mount onto a path on a process's behalf, so this is +# the one Unix-classic escape hatch (setuid-root, re-derives its own +# authorization from the executive, never trusts its caller). NOT a VMS +# image: no ".EXE" suffix, not installed under SYS$SYSTEM:, invoked by an +# absolute Linux path DCL never shows a user. +add_executable(vms_mount_helper + vms_mount_helper.c +) + +target_include_directories(vms_mount_helper PRIVATE + ${VMS_INCLUDE_DIR} +) + +target_compile_definitions(vms_mount_helper PRIVATE + _POSIX_C_SOURCE=200809L + _DEFAULT_SOURCE +) + +target_compile_options(vms_mount_helper PRIVATE + -Wall -Wextra +) + +target_link_libraries(vms_mount_helper PRIVATE vms) + +set_target_properties(vms_mount_helper PROPERTIES + OUTPUT_NAME "vms_mount_helper" +) diff --git a/tools/vms_mount_helper.c b/tools/vms_mount_helper.c new file mode 100644 index 000000000..f6ad58250 --- /dev/null +++ b/tools/vms_mount_helper.c @@ -0,0 +1,107 @@ +/* + * vms_mount_helper.c - the ONLY thing on the node still Linux root when + * DCL's MOUNT/DISMOUNT need mount(2)/umount(2) (vms-651). + * + * WHY THIS EXISTS. LOGINOUT setuid()/setgid()'s every VMS session onto its + * SYSUAF UIC (vms_login.c) -- VMS privilege is not Linux capability, and + * OVMX does not blur that line by handing a VMS session real Linux root or + * CAP_SYS_ADMIN just because it holds PRV$M_MOUNT. So by the time + * cmd_mount()/cmd_dismount() (src/vmsdcl/dcl_cmd_misc.c) know MOUNT is + * authorized, the calling process cannot call mount(2)/umount(2) itself -- + * both require CAP_SYS_ADMIN unconditionally, regardless of VMS privilege. + * + * A kernel-mediated mount (a new vms.ko ioctl calling the mount-syscall + * internals directly) was tried first and does NOT work on this kernel: + * fs/namespace.c's do_mount()/path_mount()/do_umount()/path_umount() are + * NOT in the exported-symbol table at all (confirmed against this build's + * own Module.symvers, not guessed), and the public replacement API + * (fs_context_for_mount()/vfs_get_tree()/vfs_create_mount()) has no + * exported way to GRAFT the resulting mount onto a path in a process's + * namespace -- that step is deliberately kept private to the mount(2) + * syscall's own implementation. A kernel module mounting an arbitrary path + * on a process's behalf is not a supported operation on this platform. + * + * THE DESIGN THIS SETTLES ON INSTEAD -- the classic Unix shape (mount(8), + * su, ping, ... were all setuid-root for the same reason): a small, + * setuid-root helper that does ONLY mount(2)/umount(2) of vmsfs on an + * already-existing directory, and re-derives its OWN authorization from + * the executive before doing anything privileged -- it does not trust + * whoever invoked it, including its own parent. + * + * WHY RE-DERIVING AUTHORIZATION HERE IS NOT OPTIONAL. This binary is + * setuid-root on disk; if it trusted its caller, ANY Linux process on the + * node (any VMS session, any UID) could invoke it directly with crafted + * arguments and mount/unmount anything, bypassing PRV$M_MOUNT entirely. + * Instead it CONTINUES the calling process's VMS identity onto itself + * (vms_kif_register_continue() -- the SAME "forked child before doing + * privileged work inherits the parent's authenticated identity" shape + * image activation already uses, tools/vms_login.c's caller in + * dcl_activate_image) and asks the executive whether PRV$M_MOUNT is held + * (vms_kif_chkpriv, vms_ioctl_chkpriv, src/kernel/vms_access.c -- checked + * against proc->cur_privs, kernel-resident state). A process invoked + * directly, outside that fork-before-exec sequence, registers as a fresh, + * unauthenticated process with zero privileges and is refused here, exactly + * as the executive would refuse it anywhere else. + * + * cmd_mount()/cmd_dismount() are the only product callers: they fork(), + * call vms_kif_register_continue() in the child (so THIS process's pid is + * bound to the parent's VMS identity before exec() replaces the image -- + * pid/tgid survive exec, uid/gid/capabilities do not), then execv() this + * binary. + * + * Deliberately minimal: no VMS filespec resolution, no DCL parsing, no + * qualifiers -- it takes exactly the two forms below and refuses anything + * else. Every argument DCL hands it is already a resolved Linux path. + * + * vms_mount_helper mount + * vms_mount_helper umount + * + * Exit 0 on success. Exit 1 with "ERRNO " on stderr if the + * mount(2)/umount(2) itself failed (the caller renders this honestly -- + * there is no oracle-pinned VMS status for a failed Linux mount syscall, + * CLAUDE.md Rule 10). Exit 2 for a usage error. Exit 3 if PRV$M_MOUNT is + * not held. + */ + +#include +#include +#include +#include + +#include "vms_kif.h" +#include "prvdef.h" + +int main(int argc, char *argv[]) +{ + /* Continue the CALLER's VMS identity onto this process -- see the + * file header for why this, and not trusting argv, is the actual + * security boundary. */ + (void)vms_kif_open(); + (void)vms_kif_register_continue(); + + uint32_t priv_status = vms_kif_chkpriv(PRV$M_MOUNT); + if (!(priv_status & 1)) { + fprintf(stderr, "NOPRIV\n"); + return 3; + } + + if (argc == 4 && strcmp(argv[1], "mount") == 0) { + if (mount(argv[2], argv[3], "vmsfs", 0, NULL) != 0) { + fprintf(stderr, "ERRNO %d %s\n", errno, strerror(errno)); + return 1; + } + return 0; + } + + if (argc == 3 && strcmp(argv[1], "umount") == 0) { + if (umount(argv[2]) != 0) { + fprintf(stderr, "ERRNO %d %s\n", errno, strerror(errno)); + return 1; + } + return 0; + } + + fprintf(stderr, "usage: vms_mount_helper mount \n" + " vms_mount_helper umount \n"); + return 2; +} From 1a6cd0036a118b4ca7206a5901ed4d7e24cd3ac8 Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 10 Aug 2026 10:49:01 +0000 Subject: [PATCH 2/3] test: F$GETJPI CURPRIV/AUTHPRIV now renders MOUNT (vms-651 fallout) VMS_PRV_M_ENFORCED gained PRV$M_MOUNT (bit 17, right after WORLD's bit 16) in the previous commit -- cmd_mount/cmd_dismount really gate on it now, so it belongs in the enforced set F$GETJPI CURPRIV/AUTHPRIV render (they walk VMS_PRV_M_ENFORCED bit by bit, ascending). SYSTEM/ALL's rendered string gains ",MOUNT" at the end. Measured against a real CI run (Kernel Executive + VMS User Acceptance Test jobs both failed on this exact string before the fix): three hardcoded expectations updated to match -- tests/qemu/test_syssvc_ident.c scenario F, its facility_defects.sh manifest line, and tests/uat/vms_session_qemu.sh's CURPRIV/AUTHPRIV checks (whose own comment already documented the update rule this follows). Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/qemu/facility_defects.sh | 2 +- tests/qemu/test_syssvc_ident.c | 12 ++++++++++-- tests/uat/vms_session_qemu.sh | 12 +++++++----- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/qemu/facility_defects.sh b/tests/qemu/facility_defects.sh index 46d49c7ff..35525259b 100755 --- a/tests/qemu/facility_defects.sh +++ b/tests/qemu/facility_defects.sh @@ -2686,7 +2686,7 @@ C: the executive refused an unprivileged process's attempt to become SYSTEM (SS$ C: the privilege display is EMPTY -- the two privileges the executive granted an unprivileged process (TMPMBX, NETMBX) are both outside VMS_PRV_M_ENFORCED D: the session established its authenticated identity F: the executive accepted the SYSTEM/ALL identity this scenario needs (cur_privs = ~0ULL, so every VMS_PRV_M_ENFORCED bit is set) -F: F$GETJPI CURPRIV renders SYSTEM/ALL's actual enforced privilege names (CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD), not merely completes without rendering anything +F: F$GETJPI CURPRIV renders SYSTEM/ALL's actual enforced privilege names (CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD,MOUNT), not merely completes without rendering anything G: the session established an authenticated identity G: the executive HOLDS that name and reads it back -- so the subprocess's blank below is not the executive naming nobody G/OPCOM+: the named run established its identity through the executive (without this the header check below is about a process that is also unnamed) diff --git a/tests/qemu/test_syssvc_ident.c b/tests/qemu/test_syssvc_ident.c index 04110bc79..0a39c228a 100644 --- a/tests/qemu/test_syssvc_ident.c +++ b/tests/qemu/test_syssvc_ident.c @@ -1849,9 +1849,17 @@ int main(void) "F: the executive accepted the SYSTEM/ALL identity this scenario " "needs (cur_privs = ~0ULL, so every VMS_PRV_M_ENFORCED bit is set)"); /* negctl-knockon: bind-client-no-register */ - CHECK(strstr(outf, "IDENT_CURPRIV = \"CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD\"") != NULL, + /* vms-651 added PRV$M_MOUNT (bit 17, right after WORLD's bit 16) to + * VMS_PRV_M_ENFORCED -- cmd_mount/cmd_dismount now really gate on it + * (src/vmsdcl/dcl_cmd_misc.c), so it is no longer merely stored and + * reported; it belongs in the enforced set F$GETJPI CURPRIV walks, + * and MOUNT is the name that set VMS_PRIV_NAME_LIST already carried + * for it. The render loop walks bits 0..63 in ascending order + * (src/vmsdcl/dcl_lexical.c lex_getjpi()), so MOUNT (17) renders + * immediately after WORLD (16). */ + CHECK(strstr(outf, "IDENT_CURPRIV = \"CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD,MOUNT\"") != NULL, "F: F$GETJPI CURPRIV renders SYSTEM/ALL's actual enforced " - "privilege names (CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD), " + "privilege names (CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD,MOUNT), " "not merely completes without rendering anything"); } diff --git a/tests/uat/vms_session_qemu.sh b/tests/uat/vms_session_qemu.sh index d786aef02..b961bdd34 100755 --- a/tests/uat/vms_session_qemu.sh +++ b/tests/uat/vms_session_qemu.sh @@ -961,11 +961,13 @@ check_response_at "$IDX_PRIORITY_SET" 'NOPRIV' # SHOW PROCESS/PRIVILEGES's own, different, VMS display convention. # SYSNAM/GRPNAM (bit positions 2/3, between CMEXEC and SETPRV) joined # VMS_PRV_M_ENFORCED in vms-5b7 (LNM$SYSTEM/LNM$GROUP privilege -# enforcement) -- this literal is DERIVED from that mask's current -# definition, not a number owned by this test; update it again whenever -# VMS_PRV_M_ENFORCED (src/kernel/vms_ioctl.h) changes. -check_response 'SHOW SYMBOL IDENT_CURPRIV' 'IDENT_CURPRIV = "CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD"' -check_response 'SHOW SYMBOL IDENT_AUTHPRIV' 'IDENT_AUTHPRIV = "CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD"' +# enforcement); MOUNT (bit position 17, right after WORLD) joined it in +# vms-651 (cmd_mount/cmd_dismount really gate on it) -- this literal is +# DERIVED from that mask's current definition, not a number owned by this +# test; update it again whenever VMS_PRV_M_ENFORCED (src/kernel/ +# vms_ioctl.h) changes. +check_response 'SHOW SYMBOL IDENT_CURPRIV' 'IDENT_CURPRIV = "CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD,MOUNT"' +check_response 'SHOW SYMBOL IDENT_AUTHPRIV' 'IDENT_AUTHPRIV = "CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD,MOUNT"' # SET PROCESS/PRIVILEGES IS NOW WIRED TO THE EXECUTIVE (vms-e5d7). It used to # be a HIDE stub that printed %OVMX-I-NOSETPRV and changed nothing; it now From f8e5063134da3fc2c1407179c68ffe848b0a398f Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 10 Aug 2026 11:13:16 +0000 Subject: [PATCH 3/3] test: keep the negctl-knockon marker adjacent to its CHECK() (vms-651 fixup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit inserted an 8-line explanatory comment BETWEEN test_syssvc_ident.c's `/* negctl-knockon: bind-client-no-register */` marker and the CHECK() it anchors -- every other marker in this file (and the anchoring reader in facility_defects.sh) assumes the marker sits DIRECTLY above the statement it names, with nothing in between. Moved the explanatory comment above the marker instead, restoring that adjacency. Measured against a real CI run (Kernel Executive — Per-Facility Negative Controls, shard 0/6): it reported "test_syssvc_ident.c:1851 anchors 'bind-client-no-register' but the statement under it names none of that defect's require_fail/knock_on_fail texts" -- the reader had walked into the explanatory comment instead of the CHECK() call two names anchors away. The same run's "derived suite(s) NAMED BY NO defect's suites_red" report (unrelated-looking suites: test_kmod_resdir, test_syssvc_rightslist, test_syssvc_sysuaf_uic_base) is suspected fallout from the same desync -- this fix's own next run tells us whether that suspicion holds. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/qemu/test_syssvc_ident.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/qemu/test_syssvc_ident.c b/tests/qemu/test_syssvc_ident.c index 0a39c228a..c26359eab 100644 --- a/tests/qemu/test_syssvc_ident.c +++ b/tests/qemu/test_syssvc_ident.c @@ -1848,7 +1848,6 @@ int main(void) CHECK(strstr(outf, "SETIDENT_STATUS=1") != NULL, "F: the executive accepted the SYSTEM/ALL identity this scenario " "needs (cur_privs = ~0ULL, so every VMS_PRV_M_ENFORCED bit is set)"); - /* negctl-knockon: bind-client-no-register */ /* vms-651 added PRV$M_MOUNT (bit 17, right after WORLD's bit 16) to * VMS_PRV_M_ENFORCED -- cmd_mount/cmd_dismount now really gate on it * (src/vmsdcl/dcl_cmd_misc.c), so it is no longer merely stored and @@ -1857,6 +1856,7 @@ int main(void) * for it. The render loop walks bits 0..63 in ascending order * (src/vmsdcl/dcl_lexical.c lex_getjpi()), so MOUNT (17) renders * immediately after WORLD (16). */ + /* negctl-knockon: bind-client-no-register */ CHECK(strstr(outf, "IDENT_CURPRIV = \"CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD,MOUNT\"") != NULL, "F: F$GETJPI CURPRIV renders SYSTEM/ALL's actual enforced " "privilege names (CMKRNL,CMEXEC,SYSNAM,GRPNAM,SETPRV,WORLD,MOUNT), "