Skip to content

vms-707: DCL RUN surfaces the image's executive-recorded $STATUS (not 0x1) - #853

Merged
baron-3dl merged 8 commits into
mainfrom
work/vms-707
Aug 28, 2026
Merged

baron-3dl merged 8 commits into
mainfrom
work/vms-707

Conversation

@baron-3dl

Copy link
Copy Markdown
Contributor

Problem (vms-707, p1 — critical path)

DCL's RUN verb forks a subprocess to activate an image and read its exit, but the fork()+execve() fallback in dcl_activate_image collapsed the POSIX child exit to SS\$_NORMAL/SS\$_ABORT, discarding the image's real VMS condition value. An image whose main returned 3 reported \$STATUS %X00000001 instead of the faithful DEC C encoding (C\$_EXIT1 + (3-1)*8 = 0x0035A019) the executive recorded at SYS\$EXIT. This blocked reading any fork-activated image's true exit status (e.g. the GCC-lane #846 crtl_rms sentinel).

The in-process activation path already read the executive-recorded \$STATUS (vms_kif_getexit); only the fork fallback collapsed it.

Fix — RUN reads the executive-recorded condition value

VMS behavior: the image records its completion condition value and the invoking CLI reads it back as \$STATUS. OVMX's fork child records its condition into its own executive PCB at SYS\$EXIT. Because that child shares DCL's VMS PID (REGISTER_CONTINUE), a by-VMS-PID read is ambiguous between DCL and the child — so RUN reads by the child's Linux pid, which names its PCB row uniquely, in the zombie window (WNOWAIT peek) before waitpid reaps it. Falls back to the POSIX-derived verdict only when no image recorded a status (no /dev/vms, or a foreign tool that never \$EXITs).

  • src/kernel/vms_ioctl.h, src/kernel-netbsd/vms_proctab_nb.h: VMS_JPI_SEL_LINUX_PID (no struct/ioctl-number change; reuses the args' vms_pid field).
  • src/kernel-core/vms_proctab.c: find_by_linux_pid + the SEL_LINUX_PID GETEXIT case (shared by the Linux vms.ko and the NetBSD module).
  • src/libvmssys/vms_kif.{c,h} + src/vmslink/libvmssys_shr.vec: vms_kif_getexit_linux wrapper (census-satisfied: DCL calls it).
  • src/vmsdcl/dcl_cmd_process.c: RUN fork path reads the recorded \$STATUS.

stdout/stderr routing

Investigated: tests/qemu/test_product_install_e2e.sh already RUNs an installed image via the fork path and asserts real console output, so fork-path stdout/stderr routing to the console already works. The BOOT-A "absent output" was the opt-in OVMX_IMGACT_SEAM line (off by default) plus that image's own path — not a routing regression. No routing change needed; the \$STATUS collapse was the substantive bug.

Proof / regression gate

tests/qemu/test_kmod_exit.c Part 5 (kernel-executive QEMU suite, real /dev/vms): a REGISTER_CONTINUE child records 0x0035A019, the parent confirms it shares DCL's VMS PID, then reads the child's \$STATUS by Linux pid in the zombie window — the exact dcl_activate_image sequence — and asserts the EXACT condition round-trips (not 0x1), plus a negctl that an unbacked Linux pid reads SS\$_NONEXPR.

The \$STATUS symbol chain is verified end-to-end: cmd_rundcl_activate_image returns conddcl_set_status renders %X0035A019 into the \$STATUS symbol read by SHOW SYMBOL \$STATUS.

🤖 Generated with Claude Code

baron-3dl and others added 5 commits August 28, 2026 16:55
…exit

The fork()+execve() fallback in dcl_activate_image collapsed the POSIX child
exit to SS$_NORMAL/SS$_ABORT, discarding the image's real VMS condition value:
an image whose main returned 3 reported $STATUS %X00000001 instead of the
faithful DEC C encoding the executive recorded at SYS$EXIT.

Wire RUN's fork path to read the image's executive-recorded completion $STATUS,
mirroring the in-process path. Because the forked child shares DCL's VMS PID
(REGISTER_CONTINUE), a by-VMS-PID read is ambiguous; add a by-Linux-pid GETEXIT
selector (VMS_JPI_SEL_LINUX_PID) and read the child's row before waitpid reaps
it (WNOWAIT peek). Fall back to the POSIX-derived verdict only when no image
recorded a status (no /dev/vms, or a foreign tool that never $EXITs).

- src/kernel/vms_ioctl.h, src/kernel-netbsd/vms_proctab_nb.h: SEL_LINUX_PID (no
  struct/ioctl-number change; reuses the args' vms_pid field for the Linux pid).
- src/kernel-core/vms_proctab.c: find_by_linux_pid + SEL_LINUX_PID case (shared
  by the Linux vms.ko and the NetBSD module).
- src/libvmssys/vms_kif.{c,h}: vms_kif_getexit_linux wrapper.
- src/vmslink/libvmssys_shr.vec: append vms_kif_getexit_linux (append-only).
- src/vmsdcl/dcl_cmd_process.c: RUN fork path reads the recorded $STATUS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend test_kmod_exit (kernel-executive QEMU suite, real /dev/vms) with Part 5:
a REGISTER_CONTINUE child records COND_RUN3 (0x0035A019 -- the faithful DEC C
encoding of main returning 3), the parent confirms the child SHARES its VMS PID
(so a by-VMS-PID read is ambiguous), then reads the child's $STATUS by its Linux
pid in the zombie window (WNOWAIT peek, before reap) -- the exact sequence
dcl_activate_image now runs -- and asserts the EXACT condition value round-trips,
not a POSIX-collapsed 0x1. Includes a negctl: an unbacked Linux pid reads
SS$_NONEXPR, never a fabricated status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… $STATUS

Root cause the SEL_LINUX_PID readback exposed: the executive PCB is keyed on the
thread group, not the /dev/vms channel, and vms_dev_release() frees the PCB when
the LAST channel closes at the owning task's exit. DCL's RUN fork child opens a
channel via REGISTER_CONTINUE and then execve()s the image; because /dev/vms was
opened WITHOUT O_CLOEXEC, that channel LEAKED into the image (no userspace handle
referenced it), stayed open across the whole run, and its implicit close at the
image's do_exit() freed the PCB -- destroying the image's recorded completion
$STATUS before DCL could read it back. So RUN would always fall back to the
POSIX-collapsed status even with the readback wired.

Open /dev/vms O_CLOEXEC. The channel now closes at execve() (not a free -- the
task is not exiting), the image re-binds its own channel (kif_bind) and closes it
before SYS$EXIT, so at do_exit() no channel is open and the PCB survives to lazy
reap -- long enough for RUN to read the recorded $STATUS in the waitid(WNOWAIT)
window. Nothing relies on inheriting a /dev/vms channel across execve: the
executive keys on the thread group and every task re-binds on its first kif call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…S peek

DCL's RUN fork path uses waitid(WNOWAIT) to peek the exited image without reaping
it, so the executive PCB survives long enough to read the recorded $STATUS. The
VMS-native LINK.EXE build resolves every external against a --use'd shareable's
symbol vector, so DCL.EXE referencing waitid failed with %LINK-F-ERROR until
waitid was a DECC$SHR universal. waitid is a POSIX.1 C RTL entry point; add it to
the DECC$SHR CRTL surface map (musl-backed, alongside wait3/wait4/waitpid).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or placement)

The prior commit added waitid to decc_crtl_map.txt, but that map produces the
DECORATED decc$waitid alias (for alpha-dec-vms port objects), while DCL is x86_64/
aarch64 musl-compiled and references the BARE waitid -- which resolves against the
curated bare-universal block in mk_decc_shr.sh, not the decc$ alias map. DCL.EXE
still failed %LINK-F-ERROR 'waitid'.

Revert the map edit (its mid-list insertion also shifted decc$ alias indices) and
instead append bare waitid=PROCEDURE at the very END of the DECC$SHR symbol vector
-- append-only, so no prior universal's index moves (GSMATCH LEQUAL-compatible).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nce gate)

Add a minimal VMS-native RUN target (src/apps/rctest/rc3.c + mk_rc3.sh, same
cc->LINK.EXE toolchain as PARTS.EXE) that prints one line to SYS$OUTPUT and exits
cleanly; master it into SYS$SYSTEM: on the bootable disk.

tests/qemu/test_run_status_e2e.sh boots the runtime, logs in SYSTEM/MANAGER, RUNs
RC3.EXE, and asserts the reworked RUN fork path (waitid(WNOWAIT) peek -> executive
$STATUS readback by Linux pid -> reap) still, on the real runtime: (1) routes the
activated image's stdout to the console, and (2) reports SS$_NORMAL for the clean
exit through the new readback path (never a hang, spurious error, or wrong value).

The FAITHFUL-ENCODING half (a bit<0>-set condition C$_EXIT1+(N-1)*8 surviving to
$STATUS instead of collapsing to %X00000001) is an Alpha GCC-port property (IMGACT
VMS-standard activation, which x86_64 lacks -- imgact.c stub): proven at the
executive level by test_kmod_exit.c Part 5 (real /dev/vms) and end-to-end by the
Alpha crtl_rms re-run this change unblocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl and others added 2 commits August 28, 2026 18:12
run_run_status_e2e.sh + CMake add_test mirror dcl_acceptance_e2e: opt-in behind
OVMX_QEMU_FULL_E2E=1, SKIP_RETURN_CODE 77, TIMEOUT 1200. The gate boots the real
runtime, RUNs RC3.EXE, and asserts the reworked RUN fork path routes the image's
stdout to the console and reports its completion status via the new
waitid-peek/getexit readback path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	tests/qemu/CMakeLists.txt
@baron-3dl
baron-3dl merged commit cfcd5b5 into main Aug 28, 2026
5 checks passed
baron-3dl added a commit that referenced this pull request Aug 28, 2026
…pped gap)

This arch-asymmetric boot regression slipped the per-PR gate TWICE now (#835
tracepoints, #853 O_CLOEXEC/O_DIRECT): a change reds the OVMX/Alpha boot while
x86_64 stays green, so the author's x86_64-only proofs pass and it merges. The
existing per-PR Alpha guard (vms-5af) is BUILD/modpost ONLY -- it proves vms.ko
cross-compiles + links + modpost-passes for Alpha, never that the assembled
runtime BOOTS and MOUNTS the system disk. #853 modpost-passed cleanly for Alpha
yet halted at $MOUNT.

Add job alpha-boot-mount: on a pull_request that touches the runtime-executive
change classes (new path filter alpha_boot_runtime: src/kernel/**,
src/kernel-core/**, src/libvmssys/**, src/ovmx_init/**, tools/cross-alpha/**),
run tools/cross-alpha/run-boot-alpha.sh gate -- boot qemu-system-alpha to a real
DCL Username:, which proves the DKA0: $MOUNT (mount is a strict prerequisite of
reaching login). Scope-gated so the fast PR wall is preserved for the majority
of PRs that don't touch the executive; PR-only, since schedule/workflow_dispatch
already run the fuller alpha-boot-login / alpha-dcl-acceptance legs. The boot's
dominant cost is the cross-build (identical for a mount-only vs Username
assertion), so the stronger post-condition is asserted for the same wall time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl added a commit that referenced this pull request Aug 28, 2026
… on Linux; Alpha O_CLOEXEC != O_DIRECT) (#867)

* vms-707: fix Alpha /dev/vms open -- O_ flag ABI is per-arch on Linux (Alpha O_CLOEXEC != generic)

PR #853 (cfcd5b5) added O_CLOEXEC to the /dev/vms open in kif_xport_dev_open()
so DCL's RUN fork child's channel closes at execve and the executive PCB
survives for the $STATUS readback. That was correct on x86_64/aarch64 but broke
the Alpha boot: the system disk would not $MOUNT and PID 1 halted with
%OVMX-F-SYSINIT, system disk DKA0: (/dev/vda) would not mount.

Root cause -- NOT a PCB-lifecycle problem. The freestanding libvmssys O_* flag
constants (vms_types.h, Linux raw-syscall ABI block) were hardcoded to the
asm-generic numbering used by x86_64/aarch64. Alpha inherits the OSF/1-derived
numbering (arch/alpha/include/uapi/asm/fcntl.h), where the octal codes differ:
the GENERIC O_CLOEXEC bit 0x80000 is O_DIRECT on Alpha (verified against the
alpha-linux-gnu sysroot header). So on Alpha, open("/dev/vms", O_RDWR|0x80000)
set O_DIRECT on a char device whose address_space has no ->direct_IO -- the VFS
rejects that open with -EINVAL, the kif fd came back negative, and the ACP
$MOUNT ioctl ran on a bad fd and failed. Arch-asymmetric: the exact bit that is
O_CLOEXEC on x86_64 is O_DIRECT on Alpha, which is why #853's x86_64-only proofs
passed and this slipped the per-PR gate.

Fix: split the Linux O_* block by arch. Under __alpha__, define the OSF/1
values (O_CLOEXEC 0x200000, O_DIRECTORY 0x8000, O_CREAT 0x200, O_TRUNC 0x400,
O_APPEND 0x8, O_NONBLOCK 0x4, O_EXCL 0x800, O_NOCTTY 0x1000); x86_64/aarch64
keep the generic values unchanged. O_RDONLY/O_WRONLY/O_RDWR (0/1/2) and the AT_*
constants are identical on every Linux arch, so only the divergent flags split.
A _Static_assert guards the exact hazard: on Alpha VMS_O_CLOEXEC must not be the
generic 0x80000 (O_DIRECT). Values verified against the alpha-linux-gnu
uapi/asm/fcntl.h; header compile-checked on both alpha-linux-gnu-gcc and gcc.

This preserves BOTH: Alpha now opens /dev/vms with a real O_CLOEXEC bit (mount
works, boots to Username:) and x86_64 keeps vms-707's RUN $STATUS readback. The
latent landmine that all the OTHER O_* flags (O_CREAT/O_TRUNC/... used by
vms_stdio.c) were also wrong on Alpha is fixed by the same split.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vms-707: per-PR Alpha runtime boot-to-mount gate (close the twice-slipped gap)

This arch-asymmetric boot regression slipped the per-PR gate TWICE now (#835
tracepoints, #853 O_CLOEXEC/O_DIRECT): a change reds the OVMX/Alpha boot while
x86_64 stays green, so the author's x86_64-only proofs pass and it merges. The
existing per-PR Alpha guard (vms-5af) is BUILD/modpost ONLY -- it proves vms.ko
cross-compiles + links + modpost-passes for Alpha, never that the assembled
runtime BOOTS and MOUNTS the system disk. #853 modpost-passed cleanly for Alpha
yet halted at $MOUNT.

Add job alpha-boot-mount: on a pull_request that touches the runtime-executive
change classes (new path filter alpha_boot_runtime: src/kernel/**,
src/kernel-core/**, src/libvmssys/**, src/ovmx_init/**, tools/cross-alpha/**),
run tools/cross-alpha/run-boot-alpha.sh gate -- boot qemu-system-alpha to a real
DCL Username:, which proves the DKA0: $MOUNT (mount is a strict prerequisite of
reaching login). Scope-gated so the fast PR wall is preserved for the majority
of PRs that don't touch the executive; PR-only, since schedule/workflow_dispatch
already run the fuller alpha-boot-login / alpha-dcl-acceptance legs. The boot's
dominant cost is the cross-build (identical for a mount-only vs Username
assertion), so the stronger post-condition is asserted for the same wall time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baron-3dl added a commit that referenced this pull request Aug 28, 2026
…E parity (vms-a5d, vms-431) (#877)

Two CI gates whose checks never ran at merge (YAML-broken-CI blind window).
Each fix makes the gate assert the CORRECT current product behavior; neither
weakens, allowlists-out, or deletes a gate (INV-6).

RED 2 (vms-a5d) — tests/dcl/test_lexical_getsyi.sh:
  F$GETSYI("VERSION") now emits the FAITHFUL fixed 8-char space-padded
  SYI$_VERSION field ("V9.2-3  "), landed by #866/vms-28a
  (ovmx_compat_version_field, OVMX_VMS_VERSION_FIELD_LEN=8). The old regex
  required the closing quote immediately after the version, so the faithful
  trailing pad failed it. Updated to (1) assert the version VALUE is well-formed,
  tolerating the pad, and (2) POSITIVELY assert the field is exactly 8 chars
  wide (regex X = "[^"]{8}") — the padding is the point, so the test asserts it
  rather than ignoring it. Value assertion + EXPECT_NOT V7.3/%DCL- preserved.
  Verified: built DCL.EXE emits `X = "V9.2-3  "`; harness reports
  `PASS: F$GETSYI returns system information`. A mis-padded 7-char field is
  still rejected (teeth intact). Connects to vms-f5d.

RED 3 (vms-431) — tools/parity/image-parity-allowlist.json:
  RC3.EXE (the x86_64 DCL $STATUS-propagation RUN fixture, #853/vms-707) is a
  LINK.EXE-built VMS-native image (zero DT_NEEDED/DT_HASH, PT_INTERP=IMGACT.EXE)
  staged into SYS$SYSTEM: so `RUN SYS$SYSTEM:RC3.EXE` exercises the reworked
  IMGACT.EXE-over-ACP RUN path. It is legitimately x86_64-only: VAX activates
  images via NetBSD's own ld.elf_so (Decision A, vms-42d) — the same reason
  IMGACT.EXE itself is already allowlisted — and no VAX test references RC3
  (it appears only under tests/qemu/). Added a Decision-A allowlist entry with
  that documented reason; did NOT blind-allowlist.
  Verified: parity gate suite 17/17 pass; x86_64_only == set(); RC3.EXE shows
  in allowlisted_x86_64_only. Gate teeth proven by test_unallowlisted_gap_fails.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant