Skip to content
Merged
15 changes: 15 additions & 0 deletions distro/Dockerfile.bootable
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ RUN CC=gcc CFLAGS="-fPIC -O2 -ffreestanding -fno-builtin -fno-stack-protector -m
"/tmp/build/link-native/SYSLIB/DECC\$SHR.EXE" \
"/tmp/build/link-native/SYSLIB/LIBVMSRMS\$SHR.EXE"

# RC3.EXE (vms-707): a minimal RUN target for the DCL $STATUS-propagation gate --
# prints one line to SYS$OUTPUT and terminates with a nonzero completion status.
# Same cc -> LINK.EXE toolchain + producer images as PARTS.EXE above, so it
# activates through IMGACT.EXE over the ACP exactly as any RUN target does.
# tests/qemu/test_run_status_e2e.sh boots the runtime, RUNs it, and asserts DCL's
# $STATUS is the image's executive-recorded completion status (not a blanket
# %X00000001) AND that the image's stdout reached the console.
RUN CC=gcc CFLAGS="-fPIC -O2 -ffreestanding -fno-builtin -fno-stack-protector -mtls-dialect=gnu2" \
sh src/apps/rctest/mk_rc3.sh \
/tmp/build/bin/LINK.EXE \
/tmp/build/link-native/RC3.EXE \
"/tmp/build/link-native/SYSLIB/DECC\$SHR.EXE"

# Ground-source gate, baked into the build itself (not just asserted by a
# separate CI job elsewhere): every artifact this stage ships must be a real
# LINK.EXE-built x86_64 image with NO DT_NEEDED/DT_HASH (LINK.EXE emits no
Expand Down Expand Up @@ -405,6 +418,7 @@ COPY --from=link-native /tmp/build/link-native/SYSLIB /link-native/SYSLIB
COPY --from=link-native /tmp/build/link-native/SYSEXE /link-native/SYSEXE
COPY --from=link-native /src/src/imgact/IMGACT.EXE /link-native/IMGACT.EXE
COPY --from=link-native /tmp/build/link-native/PARTS.EXE /link-native/PARTS.EXE
COPY --from=link-native /tmp/build/link-native/RC3.EXE /link-native/RC3.EXE

# KVER is the from-source kernel's kernelrelease (copied from kernel-build as
# /tmp/kver above, e.g. 6.18.44-ovmx). The build tree lives at
Expand Down Expand Up @@ -521,6 +535,7 @@ RUN mkdir -p /system-stage/dev /system-stage/proc /system-stage/sys \
# static+dynamic initramfs, DCL as the flagship dynamic proof.
cp build-static/bin/STARTUP.EXE /system-stage/init && \
cp /link-native/IMGACT.EXE /system-stage/vms/SYS0/SYSCOMMON/SYSEXE/ && \
cp /link-native/RC3.EXE /system-stage/vms/SYS0/SYSCOMMON/SYSEXE/ && \
cp "/link-native/SYSEXE/DCL.EXE" /system-stage/vms/SYS0/SYSCOMMON/SYSEXE/ && \
cp "/link-native/SYSEXE/LOGINOUT.EXE" /system-stage/vms/SYS0/SYSCOMMON/SYSEXE/ && \
cp /link-native/SYSLIB/*.EXE /system-stage/vms/SYS0/SYSCOMMON/SYSLIB/ && \
Expand Down
25 changes: 25 additions & 0 deletions src/apps/rctest/mk_rc3.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/sh
# mk_rc3.sh - build RC3.EXE, a minimal RUN target for the DCL $STATUS gate
# (vms-707). Same toolchain as mk_parts.sh: cc compiles rc3.c, then LINK.EXE
# links it into a VMS-native ET_DYN executable (PT_INTERP=IMGACT.EXE, NO
# ld/ld.so, NO DT_NEEDED/DT_HASH) whose libc externals bind to DECC$SHR at
# activation. Usage: mk_rc3.sh <LINK.EXE> <out-RC3.EXE> <DECC$SHR.EXE> [src-dir]
set -e
LINK_EXE=${1:?usage: mk_rc3.sh <LINK.EXE> <out-RC3.EXE> <DECC$SHR> [src-dir]}
OUT=${2:?need output RC3.EXE path}
DECC_SHR=${3:?need DECC\$SHR.EXE}
HERE=$(cd "$(dirname "$0")" && pwd)
SRC=${4:-$HERE}
CC=${CC:-gcc}
[ -f "$DECC_SHR" ] || { echo "mk_rc3: producer image not found: $DECC_SHR"; exit 1; }
[ -f "$SRC/rc3.c" ] || { echo "mk_rc3: rc3.c not found in $SRC"; exit 1; }
WORK=${WORK:-/tmp/mk-rc3}
mkdir -p "$WORK"
CFLAGS="${CFLAGS:--fPIC -O2 -ffreestanding -fno-builtin -fno-stack-protector -mno-outline-atomics}"
DEFS="-D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE"
echo "mk_rc3: cc rc3.c"
# shellcheck disable=SC2086
$CC $CFLAGS $DEFS -x c -c -o "$WORK/rc3.o" "$SRC/rc3.c"
echo "mk_rc3: LINK.EXE --executable --use DECC\$SHR -> $OUT"
"$LINK_EXE" --executable --use "$DECC_SHR" -o "$OUT" "$WORK/rc3.o"
echo "mk_rc3: created $OUT"
30 changes: 30 additions & 0 deletions src/apps/rctest/rc3.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* rc3.c - a minimal RUN target for the DCL RUN-path acceptance gate (vms-707).
*
* The image prints one line to SYS$OUTPUT and exits successfully. RUN's fork
* path was reworked (waitid(WNOWAIT) peek -> executive $STATUS readback by Linux
* pid -> reap); this image is the end-to-end smoke test on the real runtime that
* the reworked path still (a) activates the image, (b) routes its stdout to the
* console, and (c) reports the image's completion status -- SS$_NORMAL for a
* clean exit, read back through the new path, never a hang or a wrong status.
*
* The FAITHFUL-ENCODING half of vms-707 -- a bit<0>-set completion condition
* (C$_EXIT1 + (N-1)*8) surviving to DCL's $STATUS instead of collapsing to
* %X00000001 -- is exercised by the Alpha GCC-port images (crtl_rms), whose
* IMGACT VMS-standard activation records the condition and releases the channel
* before exit; x86_64 has no such activation path (imgact.c is a stub there), so
* that half is proven at the executive level by tests/qemu/test_kmod_exit.c
* (Part 5, real /dev/vms) and end-to-end by the Alpha crtl_rms re-run.
*
* Built with the same cc -> LINK.EXE toolchain as PARTS.EXE (VMS-native ET_DYN,
* PT_INTERP=IMGACT.EXE), so it activates through the real image activator over
* the ACP exactly as any RUN target does.
*/
#include <stdio.h>

int main(void)
{
printf("RC3: image output reached SYS$OUTPUT (RUN routed the child stdout)\n");
fflush(stdout);
return 0;
}
30 changes: 30 additions & 0 deletions src/kernel-core/vms_proctab.c
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,29 @@ static struct vms_proc *find_by_vms_pid(uint32_t vms_pid)
return NULL;
}

/*
* find_by_linux_pid - locate a process by the Linux pid (thread-group id)
* backing its PCB. Unlike find_by_vms_pid(), this is unambiguous even when
* several rows SHARE one VMS PID: an image DCL activated through the
* fork()+execve() fallback runs as a continuation of DCL's identity
* (VMS_IOCTL_REGISTER_CONTINUE) and so carries DCL's VMS PID, but its own
* distinct Linux pid. This is the lookup behind VMS_JPI_SEL_LINUX_PID, the
* primitive RUN uses to read that child's true completion $STATUS (vms-707).
*
* Caller must hold vms_proc_hash_lock.
*/
static struct vms_proc *find_by_linux_pid(uint32_t linux_pid)
{
struct vms_proc *proc;
int bkt;

exec_hash_for_each(vms_proc_hash, bkt, proc, hash_node) {
if ((uint32_t)proc->linux_pid == linux_pid)
return proc;
}
return NULL;
}

/*
* vms_ioctl_hiber - $HIBER, executive-resident and AST-interruptible (vms-feb).
*
Expand Down Expand Up @@ -1122,6 +1145,13 @@ long vms_ioctl_getexit(struct vms_proc *proc, unsigned long arg)
case VMS_JPI_SEL_PID:
target = find_by_vms_pid(args.vms_pid);
break;
case VMS_JPI_SEL_LINUX_PID:
/* By backing Linux pid (vms-707): the args' vms_pid field carries the
* Linux pid here. RUN reads its fork()+execve() child's true $STATUS
* this way -- the child shares DCL's VMS PID, so only the Linux pid
* names its PCB row unambiguously. */
target = find_by_linux_pid(args.vms_pid);
break;
default:
exec_unlock(&vms_proc_hash_lock);
args.status = SS__BADPARAM;
Expand Down
1 change: 1 addition & 0 deletions src/kernel-netbsd/vms_proctab_nb.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
#define VMS_JPI_SEL_SELF 0 /* the calling process */
#define VMS_JPI_SEL_PID 1 /* by vms_pid */
#define VMS_JPI_SEL_PRCNAM 2 /* by prcnam, within the caller's UIC group */
#define VMS_JPI_SEL_LINUX_PID 3 /* GETEXIT only: by backing Linux pid (vms-707) */

#define VMS_PI_V_CPUTIM 0x00000001u /* cputim is sourced */
#define VMS_PI_V_PAGEFLTS 0x00000002u /* pageflts is sourced */
Expand Down
12 changes: 12 additions & 0 deletions src/kernel/vms_ioctl.h
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,18 @@ struct vms_procinfo {
#define VMS_JPI_SEL_SELF 0 /* the calling process */
#define VMS_JPI_SEL_PID 1 /* by vms_pid */
#define VMS_JPI_SEL_PRCNAM 2 /* by prcnam, within the caller's UIC group */
/*
* VMS_JPI_SEL_LINUX_PID (GETEXIT only, vms-707): read the completion $STATUS of
* a process named by its backing Linux pid, carried in the getexit args'
* `vms_pid` field. This is the primitive DCL's RUN uses to recover the true
* condition value of an image it activated through the fork()+execve() fallback:
* that child shares DCL's VMS PID (VMS_IOCTL_REGISTER_CONTINUE), so a by-VMS-PID
* read is ambiguous between DCL and the child, but the child's Linux pid -- which
* DCL holds from fork() -- names its PCB row uniquely. Read before the child is
* reaped (waitpid); an authorized read gated by vms_proc_may_read() exactly like
* SEL_PID. GETJPI does NOT accept this selector (its switch rejects it).
*/
#define VMS_JPI_SEL_LINUX_PID 3

struct vms_getjpi_args {
uint32_t select; /* VMS_JPI_SEL_* */
Expand Down
22 changes: 19 additions & 3 deletions src/libvmssys/kif_transport_linux.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,25 @@

int kif_xport_dev_open(void)
{
/* AT_FDCWD (-100), O_RDWR (2). The device name is the transport's own
* knowledge, per the contract -- the policy layer never spells it. */
return vms_sys_openat(-100 /* AT_FDCWD */, "/dev/vms", 2 /* O_RDWR */, 0);
/* AT_FDCWD (-100), O_RDWR (2) | O_CLOEXEC (02000000 == 0x80000). The device
* name is the transport's own knowledge, per the contract -- the policy
* layer never spells it.
*
* O_CLOEXEC (vms-707): a /dev/vms channel must NOT survive execve(). The
* executive PCB is keyed on the thread group, not the channel, and every
* task re-binds its own channel on its first kif call after an execve
* (kif_bind / REGISTER_CONTINUE), so an inherited descriptor is never read.
* Worse, it is actively harmful: DCL's RUN fork child opens a channel via
* REGISTER_CONTINUE and then execve()s the image; without O_CLOEXEC that
* channel LEAKS into the image (no userspace handle references it), stays
* open across the image's whole run, and its implicit close at the image's
* do_exit() is what triggers vms_dev_release() to FREE the process's PCB --
* destroying the image's recorded completion $STATUS before DCL can read it
* back. Closing the channel at execve() leaves the PCB owned solely by the
* image's own channel (which IMGACT closes before SYS$EXIT, not at exit), so
* the PCB survives to lazy reap and RUN can read the recorded $STATUS. */
return vms_sys_openat(VMS_AT_FDCWD, "/dev/vms",
VMS_O_RDWR | VMS_O_CLOEXEC, 0);
}

void kif_xport_dev_close(int fd)
Expand Down
28 changes: 28 additions & 0 deletions src/libvmssys/vms_kif.c
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,34 @@ uint32_t vms_kif_getexit(uint32_t *condition, int *has_exited)
return args.status;
}

/* Read the image-completion $STATUS of a process named by its backing Linux pid
* (vms-707). This is how DCL's RUN recovers the true condition value of an image
* it activated through the fork()+execve() fallback: the child shares DCL's VMS
* PID (REGISTER_CONTINUE), so a by-VMS-PID read is ambiguous, but the child's
* Linux pid -- which DCL holds from fork() -- names its PCB uniquely. Must be
* called BEFORE the child is reaped (waitpid), or the row is gone. *has_exited
* (if given) is nonzero iff an image actually recorded a status -- a foreign
* tool that never calls $EXIT leaves it 0, and the caller then derives $STATUS
* from the POSIX exit as before. INV-6: with no /dev/vms this returns the
* transport status and records nothing. */
uint32_t vms_kif_getexit_linux(uint32_t linux_pid, uint32_t *condition,
int *has_exited)
{
struct vms_getexit_args args;

vms_memset(&args, 0, sizeof(args));
args.select = VMS_JPI_SEL_LINUX_PID;
args.vms_pid = linux_pid; /* field carries the Linux pid here */

KIF_CALL(VMS_IOCTL_GETEXIT, &args);

if (condition)
*condition = args.condition;
if (has_exited)
*has_exited = (int)args.has_exited;
return args.status;
}

/* Record this (CLI) process's invoking command line + cliflag in the executive,
* so an image it activates reads the SAME context back (inherited from this
* PCB at REGISTER_CONTINUE time). cliflag == 0 means "no CLI" and the command
Expand Down
10 changes: 10 additions & 0 deletions src/libvmssys/vms_kif.h
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,16 @@ uint32_t vms_kif_setexit(uint32_t condition, uint32_t *exit_code);
* zero *condition, which is a legal value). */
uint32_t vms_kif_getexit(uint32_t *condition, int *has_exited);

/* Read the image-completion $STATUS of a process named by its backing Linux pid
* (vms-707). DCL's RUN uses this to recover the true condition value of an image
* it activated through the fork()+execve() fallback -- that child shares DCL's
* VMS PID, so only the child's Linux pid names its PCB unambiguously. Must be
* called BEFORE the child is reaped. *has_exited (optional) is nonzero iff an
* image actually recorded a status (a foreign tool that never $EXITs leaves it
* 0). */
uint32_t vms_kif_getexit_linux(uint32_t linux_pid, uint32_t *condition,
int *has_exited);

/* Record this CLI process's invoking command line + cliflag, for an activated
* image to inherit and read back. cliflag == 0 means "no CLI". */
uint32_t vms_kif_setcli(uint32_t cliflag, const char *command);
Expand Down
79 changes: 66 additions & 13 deletions src/vmsdcl/dcl_cmd_process.c
Original file line number Diff line number Diff line change
Expand Up @@ -1902,33 +1902,86 @@ static int dcl_activate_image_inner(struct dcl_context *ctx,
execv(linux_path, argv);
_exit(1);
} else if (pid > 0) {
/* Parent - wait for child (WUNTRACED for Ctrl-Y stop support) */
/*
* Parent. On OpenVMS the image runs IN the CLI's process and its
* completion condition value IS $STATUS. OVMX fork()s+execve()s, so the
* image records its VMS condition value in the executive PCB of this
* child (vms_kif_setexit, driven by IMGACT / sys_imgact at SYS$EXIT) and
* DCL reads it back here -- the fork fallback's authentic $STATUS path,
* mirroring the in-process path above (vms-707). Collapsing the POSIX
* child exit to SS$_NORMAL/SS$_ABORT, as this did before, discarded the
* real condition value: an image whose main returned 3 reported
* %X00000001, not the faithful DEC C encoding the executive held.
*
* Peek at the child's terminal state with WNOWAIT so its executive PCB
* is STILL PRESENT when we read it: a plain waitpid() reaps the child,
* and vms_proc_reap_dead() (run at the head of GETEXIT) then drops the
* row before we can read it. WEXITED|WSTOPPED catches both a Ctrl-Y stop
* and a true exit, the two outcomes the old WUNTRACED wait distinguished.
*/
extern volatile sig_atomic_t dcl_running_child;
dcl_running_child = (sig_atomic_t)pid;
int wstatus;
waitpid(pid, &wstatus, WUNTRACED);
dcl_running_child = 0;
if (WIFSTOPPED(wstatus)) {
/* Child was stopped by Ctrl-Y — save for CONTINUE */

siginfo_t si;
memset(&si, 0, sizeof(si));
while (waitid(P_PID, (id_t)pid, &si, WEXITED | WSTOPPED | WNOWAIT) < 0 &&
errno == EINTR)
;

if (si.si_code == CLD_STOPPED || si.si_code == CLD_TRAPPED) {
/* Child stopped by Ctrl-Y — save for CONTINUE (do NOT reap it). */
dcl_running_child = 0;
printf("\nInterrupt\n");
ctx->interrupted_pid = pid;
return SS$_ABORT;
}

/*
* The child has exited but is not yet reaped (WNOWAIT), so its zombie
* task keeps the executive PCB alive. Read the image's recorded
* completion $STATUS by the child's Linux pid BEFORE reaping -- by-Linux-
* pid because the child SHARES DCL's VMS PID (REGISTER_CONTINUE), which
* makes a by-VMS-PID read ambiguous between DCL and the child. `recorded`
* is set iff an image actually recorded a status: a genuine OVMX image
* (IMGACT routes SYS$EXIT through the executive $EXIT) does; a foreign
* tool or a shebang script that never calls $EXIT does not.
*/
uint32_t cond = 0;
int recorded = 0;
uint32_t gx = vms_kif_getexit_linux((uint32_t)pid, &cond, &recorded);

/* Now actually reap the child. */
int wstatus = 0;
while (waitpid(pid, &wstatus, 0) < 0 && errno == EINTR)
;
dcl_running_child = 0;

if (gx == SS$_NORMAL && recorded) {
/* The executive holds the image's real VMS condition value -- that
* is $STATUS. Surface an error severity exactly as the in-process
* path does, then hand back the true condition value (not a
* POSIX-derived collapse). */
if (!(cond & 1))
dcl_error("DCL", (int)(cond & 7), "ABORT",
"image %s exited with error status %%X%08X",
display_name, (unsigned)cond);
return cond;
}

/*
* No executive-recorded status (no /dev/vms, or a foreign tool that
* never called $EXIT): derive $STATUS from the POSIX outcome, exactly as
* before. vms-17f9: a nonzero exit or a killing signal is SURFACED, not
* swallowed, so a failed RUN never looks identical to a successful one.
*/
if (WIFEXITED(wstatus)) {
int exit_code = WEXITSTATUS(wstatus);
/* vms-17f9: a nonzero image exit must be SURFACED, not swallowed.
* Previously this returned SS$_ABORT silently, so a RUN that
* failed looked identical to one that succeeded (the de-risk that
* hunted a "no output" RUN, docs/derisk-vms-530-imgact-qemu.md). */
if (exit_code != 0)
dcl_error("DCL", 2, "ABORT",
"image %s exited with error status %%X%08X",
display_name, (unsigned)exit_code);
return (exit_code == 0) ? SS$_NORMAL : SS$_ABORT;
}
/* vms-17f9: a child killed by a signal (a crash) was silently dropped
* here and cmd_run fell through to SS$_NORMAL, reporting success for an
* image that never ran. Report it and fail. */
if (WIFSIGNALED(wstatus)) {
dcl_error("DCL", 4, "ABORT",
"image %s terminated abnormally (signal %d)",
Expand Down
6 changes: 6 additions & 0 deletions src/vmslink/libvmssys_shr.vec
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,9 @@ vms_kif_bg_accept=PROCEDURE
# executive cross-node RECEIVE seam reached over /dev/vms. APPENDED per the
# append-only contract -- a new universal never shifts an existing index.
vms_kif_dlm_xnode=PROCEDURE
# RUN's $STATUS readback for the fork()+execve() image-activation fallback
# (vms-707): read a forked child's executive-recorded completion $STATUS by its
# backing Linux pid, before waitpid reaps it, so DCL's $STATUS is the image's
# real VMS condition value -- not a success/fail collapse of the POSIX exit.
# APPENDED per the append-only contract -- a new universal never shifts an index.
vms_kif_getexit_linux=PROCEDURE
9 changes: 9 additions & 0 deletions src/vmslink/mk_decc_shr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,15 @@ if [ -f "$DECC_MAP" ]; then
echo "mk_decc_shr: decc\$ CRTL alias vector: exported $n_add, skipped $n_skip (musl-undefined — gap for a later rung, not faked)"
fi

# waitid (vms-707) APPENDED at the very end of the vector -> no prior universal's
# index moves (GSMATCH LEQUAL-compatible). DCL's RUN reads an activated image's
# executive-recorded completion $STATUS with waitid(P_PID, ..., WNOWAIT): a peek
# that does NOT reap the exited image, so its executive PCB survives to be read
# back before waitpid() reaps it. A bare musl universal DCL references directly
# (musl defines waitid); the curated bare-name block above is index-frozen, so it
# is appended here rather than inserted next to waitpid.
VEC="$VEC,waitid=PROCEDURE"

# Whole-archive, strict (NO --allow-undefined): a complete C-RTL shareable must
# link with zero deferred externals. libc.a first so its strong defs win; the
# loader-glue object last (it only REFERENCES __libc, which libc.a defines).
Expand Down
Loading