Skip to content

vms-b93: OVMX 0.1 -- VMS-native x86_64 dynamic linking, boot+install, real DCL.EXE/LOGINOUT.EXE via LINK.EXE - #155

Merged
baron-3dl merged 30 commits into
mainfrom
work/vms-b93-integration
Aug 7, 2026
Merged

baron-3dl merged 30 commits into
mainfrom
work/vms-b93-integration

Conversation

@baron-3dl

Copy link
Copy Markdown
Contributor

Summary

Batched integration PR for the vms-b93 epic (OVMX 0.1 pre-release: Rail B
standalone, VMS-native dynamic linking). ~22 items folded into
work/vms-b93-integration via local merges, each independently tested, plus a
post-merge veracity audit (5 independent adversarial passes against the
actual merged code -- found and fixed 2 real gaps).

Highlights: LINK.EXE gains a full x86_64 relocation path (simple relocs,
GOT-relative DATA imports, gnu2 TLSDESC, GOTPCRELX, crt0 + cross-image CALL
PLT stubs, read-only-section/jump-table relocations), wired into CMake for
both aarch64 and x86_64 (OVMX_LINK_NATIVE), DCL.EXE and LOGINOUT.EXE now
link and activate VMS-native on x86_64, INSTALL's Known Image DB is real
(binary KFE format), a SPAWN vmsfs dentry fix, a fat-initramfs install/boot
chain with a new slim-boot (Boot 3) proof, and doc updates.

Full history, item-by-item detail, and test evidence: rd item vms-b93.

Test plan

Every item tested individually during integration (ctest suites,
src/vmslink/test/* and src/imgact/test/* regressions on both aarch64 and
x86_64, real docker build + QEMU boot for the boot/install chain) --
detail in vms-b93's history. Final gate before this PR: full ctest
109/109, QEMU kernel harness 31/31, bootable docker build clean,
persistent-boot 25/25, UAT session 56/56, executive-integral 14/14.

alice and others added 30 commits August 6, 2026 17:52
emit_shareable() in src/vmslink/link.c had two pieces hardcoded to
AArch64 machine code regardless of g_out_machine: the synthesized crt0
entry stub for a main()-based --executable, and the cross-image CALL
PLT stub + import-CALL detection (gated on R_AARCH64_CALL26/JUMP26
only, so R_X86_64_PLT32 references to a producer universal never
routed through the import table). An x86_64 main()-based program, or
any x86_64 image with a cross-image CALL import, linked to garbage --
exactly DCL.EXE's shape, blocking vms-cb5f.

Adds, additively per g_out_machine (mirrors vms-8f5's e_machine gate):
  - is_call also covers R_X86_64_PLT32, so a PLT32 reference to an
    undefined-locally, --use-producer-exported symbol becomes an
    import exactly like aarch64's CALL26/JUMP26.
  - the cross-image-call reloc-apply branch gets an x86_64 case: a
    PC32-style S+A-P write targeting the PLT stub instead of the
    (absent) callee.
  - the PLT stub emitter gets an x86_64 case: `jmp *disp32(%rip)`
    (FF 25 imm32) through the import-GOT cell -- the one-instruction
    analogue of aarch64's adrp/ldr/br page+lo12 GOT load+branch.
  - the crt0 emitter gets a real x86_64 stub: mov rdi,[rsp] / lea
    rsi,[rsp+8] / lea rdx,[rsi+rdi*8+8] recovers argc/argv/envp per
    the SysV process-entry stack layout, `call main` then `mov
    edi,eax ; call exit` tails the return value into exit(). Encoding
    verified against `as`'s AT&T disassembly byte-for-byte.

New test (src/imgact/test/run_multiobj_exec_x86_64.sh, wired into CI
as multiobj-exec-x86_64): links a two-object main() program against a
hand-written producer shareable via LINK.EXE --executable --use,
activates it by executing the image directly (real kernel PT_INTERP
-> IMGACT.EXE, native x86_64, no emulation), and checks the process
really ran main(), read a real argc/argv off the stack (two runs, two
different exit codes), made a real cross-image CALL through the new
PLT stub into the producer, and exited via a cross-image exit() call.
Verified this test fails with 'unresolved external symbol' before the
fix (confirmed via stash) and passes after.

Regression: aarch64 MVP (run_test.sh), aarch64 crt0/PLT
(run_multiobj_exec.sh) and x86_64 simple-reloc (run_test_x86_64.sh)
harnesses all stay green. Full ctest suite: 106/107 pass; the one
failure (facility_attribution_negctl) is pre-existing and unrelated
(reproduces identically with this change stashed out -- host/container
site-derivation drift in kernel/vms_lock.c attribution, nothing to do
with vmslink).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-206: LINK.EXE emits real x86_64 crt0 + cross-image CALL PLT stubs

Clean automatic merge (verified: no conflict markers). vms-206 branched
before vms-2e4 landed on main; both modify src/vmslink/link.c's x86_64
relocation/emit_shareable() path (TLSDESC vs crt0/PLT32) but git's 3-way
merge resolved it without manual intervention.
…st real musl/libgcc

is_got_reloc() only recognized GOTPCREL (9) and REX_GOTPCRELX (42); plain
GOTPCRELX (41, gas's non-REX relaxable GOT-load variant) hit patch_pcrel's
default die("unsupported .text relocation"). Adds it as a third case
alongside its already-handled siblings in is_got_reloc()/patch_got() --
same flat-disp32-write codegen (S(got)+A-P, addend -4), since LINK.EXE
performs no GOT-load-to-LEA relaxation for any of the three.

Grounded empirically, not from psABI text alone:
- The system's prebuilt musl libc.a carries ZERO GOTPCRELX/REX_GOTPCRELX
  (non-PIC static build) -- confirming the gap check needed a real PIC
  object set, not just this host's default libc.a.
- A `-fPIC -fno-plt` probe (`extern void f(void); void g(void){f();}`)
  reproduces the exact instruction shape: `jmp *sym@GOTPCREL(%rip)` (ff 25
  disp32, no REX prefix -- near indirect call/jmp defaults to 64-bit
  operand size without REX.W), matching readelf's R_X86_64_GOTPCRELX.
- Alpine x86_64 libgcc.a (the exact toolchain vms-cb5f's DECC$SHR build
  uses) carries 1521 real GOTPCRELX occurrences -- e.g. `call *abort@
  GOTPCREL(%rip)` in _absvdi2.o -- confirming the gap is in libgcc.a
  (GCC's runtime calling abort/etc. through the GOT under -fno-plt), not
  libc.a itself, and closely matches vms-cb5f's reported 1449 (after its
  TLS-subsystem filter narrows the count).

Proof (done condition):
- Pre-fix LINK.EXE reproducibly dies with "%LINK-F-ERROR, unsupported
  .text relocation" on a hand-built intra-image GOTPCRELX call
  (caller.o -> callee.o, both defined, forcing the GOT slot to resolve
  internally rather than deferring as an import).
- Post-fix LINK.EXE links it; a new REAL-LOAD harness
  (src/vmslink/test/gotpcrelx_activate.c) mmaps the shareable at a
  genuine non-zero ASLR'd base, applies the .vms$rel load-bias fixup
  IMGACT would perform, and calls in -- proving the GOT cell resolves to
  the correct address under a real load, not a readelf/byte check.
  Wired into run_test_x86_64.sh as a permanent regression case.
- Whole-archiving the real Alpine x86_64 musl libc.a + libgcc.a (TLS
  subsystem filtered per vms-cb5f's precedent, an unrelated gap) with
  pre-fix LINK.EXE reproduces the exact die(); post-fix it gets past
  that point and (with --allow-undefined for legitimately deferred
  externals) succeeds end-to-end: 1586 objects, 146 GOT slots, 9610
  relocs, EM_X86_64 -- confirming this specific gap is cleared at the
  scale vms-cb5f hit it.

Base: work/vms-b93-integration (vms-206 merged). Blocks vms-cb5f.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-e5d: LINK.EXE resolves x86_64 GOTPCRELX (type 41), grounded against real musl/libgcc

Clean automatic merge, base was already work/vms-b93-integration.
…RCH=x86_64)

Reproduces vms-b65.6's aarch64 DCL.EXE-through-IMGACT proof for x86_64, per
vms-bdf's own done-condition (all four reloc/crt0/PLT beads merged):

- run_dcl_native.sh / lib_build_graph.sh: ARCH env var (default aarch64,
  unchanged) selects the one target-specific codegen flag each producer
  needs (-mno-outline-atomics on aarch64 vs -mtls-dialect=gnu2 on x86_64,
  the standing precedent from docs/design-link-x86_64-relocs.md) and the
  libvmssys arch/<ARCH>/syscall.S. Shared by run_login_native.sh unaffected
  (ARCH unset -> identical aarch64 defaults, verified against a real arm64
  musl container).
- mk_{vmsprocess,vmslnm,vmsfs,libvms,vmsrms}_shr.sh / mk_dcl.sh: CFLAGS is
  now env-overridable (${CFLAGS:-<same aarch64 default>}) so the x86_64
  caller can supply target-appropriate flags without a forked copy of each
  recipe.
- mk_decc_shr.sh: DECC$SHR must stay a non-TLS producer (LINK.EXE's
  one-TLS-object-per-image limit, vms-212 tracks the general fix). aarch64's
  libgcc.a empirically carries zero TLS-defining members; x86_64's whole-
  archives a dead-for-OVMX subsystem (GCC's IEEE 754-2008 decimal-float
  library + -fsplit-stack support) built on the TLSGD general-dynamic model
  LINK.EXE's x86_64 path doesn't implement (OVMX standardizes on gnu2/
  TLSDESC). Filtered architecture-generically by scanning archive members
  for .tdata/.tbss or TLSGD refs, not by hardcoding names -- a no-op on an
  archive with neither.
- .github/workflows/ci.yml: new dcl-native-x86_64 job, native amd64 (no
  QEMU binfmt needed -- the runner already is x86_64), ARCH=x86_64 through
  the same run_dcl_native.sh.

BLOCKED short of a green run: whole-archiving real musl libc.a on x86_64
(1345 objects) hits R_X86_64_GOTPCRELX (type 41), a relocation link.c's
x86_64 path does not recognize (is_got_reloc() only checks GOTPCREL/
REX_GOTPCRELX) -- confirmed empirically (1449 occurrences across ordinary,
load-bearing musl objects, not confinable to dead code the way the TLSGD
subsystem was). link.c is out of this item's file-domain per its own
repeated header comments ("do NOT edit them here"); see the escalation.

Also confirmed (fetched fresh): vms-206 is on work/vms-b93-integration
(16c7965) but NOT yet on main -- a second prerequisite for the executable
(not just shareable) link step once the GOTPCRELX gap is fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hole-archive gap

Re-dispatch, continuing from work/vms-cb5f now that vms-e5d (GOTPCRELX) is
merged into work/vms-b93-integration. Rebased cleanly onto
origin/work/vms-b93-integration (vms-206 crt0/PLT + vms-e5d GOTPCRELX both
present); the prior CI-job/ARCH-parameterization/CFLAGS work carried over
unchanged.

Whole-archiving real musl libc.a + libgcc.a on x86_64 got past GOTPCRELX and
hit a NEW gap: mk_decc_shr.sh's TLS filter (vms-cb5f's own prior commit)
removes members that directly define/reference TLS storage (bid64_add.o
etc, via TLSGD) but left non-TLS "glue" objects in place --
_addsub_dd.o/_addsub_sd.o/... call INTO the removed decimal-float subsystem
via a plain GOT reference to e.g. __bid64_add, which no longer has a
definer once bid64_add.o is dropped. LINK.EXE's "GOT symbol undefined"
strict die() was CORRECT given the archive it was handed -- the gap was in
the filter only removing the directly-tainted half of a connected dead-code
component, not link.c.

Fixed by making the filter a reference-graph fixed-point closure: after
seeding the direct TLS-tainted set (unchanged), repeatedly pull in any
surviving member whose undefined reference is satisfied ONLY by an already-
removed member, until nothing new is added. One nm pass over the whole
archive up front (not re-invoked per member per iteration) keeps this cheap
on libc.a's 1345 members. Verified as a no-op on aarch64's libgcc.a (366/366
members survive, 0 filtered, byte-identical DECC$SHR before/after) and pulls
in exactly the expected 148/241-surviving decimal-float + split-stack
subsystem on x86_64's.

With that fix, the full six-library producer graph + DCL.EXE (22 objects,
6 GOT, 2 TLS, 1053 ABS64-ptr, 145 imports) now link VMS-native and clean on
x86_64. Activation through IMGACT.EXE segfaults -- escalated (see PR/item
notes), not patched here: link.c/imgact.c are out of this item's file-domain
per the item's own repeated header comments, and the crash is upstream of
mk_decc_shr.sh (isolated: run_multiobj_exec_x86_64.sh's small-scale crt0/PLT
proof -- 3 imports, 1 GOT, 0 TLS -- still passes natively with correct
argc-computed exit codes, so this is scale/shape-specific to DCL.EXE's
import/reloc volume, not a general crt0/PLT regression).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DCL.EXE segfaulted the moment it was activated on x86_64. The fault PC sat in
the anonymous RWX region IMGACT.EXE maps for DECC$SHR, executing non-instruction
bytes. Single-stepping from musl's printf_core into pop_arg caught the transfer:

    movslq (%rcx,%rsi,4),%rdx     ; rdx = jumptable[1]
    add    %rcx,%rdx              ; rdx = table_base + delta
    jmp    *%rdx

with the table entry ZERO, so the jump landed on the table's own address --
inside .rodata, on the "(null)" string constant.

Root cause: parse_obj() collected relocations only for sections it bucketed
B_TEXT or B_DATA. Every relocation whose target section was B_RODATA was
discarded with no diagnostic. gcc emits each `switch` jump table into a
per-function read-only section as `.long arm - table_base`; the arms are in
.text and the table is not, so the assembler cannot fold the difference and
leaves one real R_X86_64_PC32 per arm. All of them were dropped, so every jump
table in the image came out all zero.

Not a scale bug. Nothing about 145 imports, 6 shareables, 6 GOT slots, 2 TLSDESC
entries or the 6-deep --use chain is involved: vms-206/vms-cd1/vms-2e4 passed
through this because their specimens contained no switch large enough for gcc to
build a table and never called a printf-family function with a conversion. It is
a code SHAPE that first appeared when real musl and the real DCL sources entered
the link -- 902 such relocations in libc.a, 554 in DCL's own objects.

aarch64 was never affected: aarch64 gcc resolves its jump tables at assembly
time, so its only read-only-section relocations are .eh_frame PREL32 (never
executed). Confirmed empirically on both arches.

Fix: collect relocations for B_RODATA as well, via a single bucket_is_patchable()
predicate, and emit %LINK-W-RELSKIP for any RELA section whose target is
allocatable but not flat-placed -- a silent drop is exactly how this survived
four proofs. (That diagnostic immediately surfaces one pre-existing gap:
libgcc's cpuinfo.o .init_array, tracked separately, not executed today.)

Regression gate: src/vmslink/test/run_rodata_reloc_x86_64.sh + its specimen link
a jump-table-bearing image against the real whole-archive musl DECC$SHR, activate
it through a real IMGACT.EXE, and diff the transcript against the SAME source
built by the system toolchain. It asserts the specimen still produces .rela.rodata
(so it cannot rot into a vacuous pass) and fails LOUD -- verified: it dies with
"Illegal instruction" on the pre-fix linker and passes on the fixed one. Wired
into CI as job rodata-reloc-x86_64.

Verified green: dcl-native ARCH=x86_64 (SHOW TIME + A=5, exit 0) and ARCH=aarch64,
run_multiobj_exec_x86_64.sh, src/imgact/test/run_test_x86_64.sh,
src/vmslink/test/run_test_x86_64.sh, run_test_x86_64_tls.sh, run_decc_shr.sh,
run_test.sh + run_multiobj_exec.sh under arm64.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-a66: LINK.EXE was dropping every read-only-section relocation
(includes vms-cb5f's commits: DCL.EXE x86_64 ARCH param + DECC$SHR
transitive TLS filter -- confirmed via git merge-base --is-ancestor
that cb5f is fully contained in a66's history, so cb5f is not merged
separately)

Clean automatic merge, no conflicts.
…ch64)

Wires the mk_*_shr.sh / mk_dcl.sh / mk_loginout.sh recipes (the vms-b65/c39
lib-migration chain) into `cmake --build` via a new OVMX_LINK_NATIVE option
that auto-detects on an aarch64 musl toolchain. `cmake -B build && cmake
--build build` now produces LIBVMSSYS$SHR.EXE, DECC$SHR.EXE, LIBVMSPROCESS$SHR.EXE,
LIBVMSLNM$SHR.EXE, LIBVMSFS$SHR.EXE, LIBVMS$SHR.EXE, LIBVMSRMS$SHR.EXE, DCL.EXE
and LOGINOUT.EXE via LINK.EXE -- verified EM_AARCH64 with zero DT_NEEDED
entries on all 9 artifacts.

Additive to (not a replacement of) each library's existing add_library()
target, which host ctest unit tests still link directly; on non-aarch64-musl
toolchains OVMX_LINK_NATIVE stays off with no behavior change.

Extracted the previously-inlined LIBVMSSYS$SHR recipe (duplicated in
lib_build_graph.sh) into mk_vmssys_shr.sh, the one place the mk_*_shr.sh
convention keeps it -- lib_build_graph.sh (run_dcl_native.sh/run_login_native.sh)
now calls it too, closing the exact drift risk mk_libvms_shr.sh's LIST
comment warns about. mk_vmssys_shr.sh exports vms_kif_setident
unconditionally (append-only vector), so run_login_native.sh's SYS_VEC_EXTRA
override is no longer needed. Both harnesses re-verified green end-to-end in
an aarch64 musl container after the refactor.

Added CI job link-native-cmake asserting the readelf ground-source condition
directly. x86_64 extension is vms-6da (separate item, unblocked).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	src/imgact/test/lib_build_graph.sh
Extends vms-b6a's OVMX_LINK_NATIVE mechanism (auto-detected from the
compiler's -dumpmachine triple) to also recognize an x86_64 musl
toolchain, not just aarch64 -- same CMake target (link_native_graph),
same build_link_native.sh entrypoint, no forked plumbing. ARCH is
threaded through as an env var to every mk_*_shr.sh recipe (CFLAGS
picks -mtls-dialect=gnu2 on x86_64 vs -mno-outline-atomics on aarch64,
the same convention lib_build_graph.sh's build_producer_graph()
established for the raw-harness path in vms-cb5f/vms-a66).

mk_vmssys_shr.sh (extracted by vms-b6a, so it hadn't picked up the
ARCH/CFLAGS env-override convention yet) and mk_loginout.sh (missed by
cb5f/a66) are brought in line with the rest of the mk_*_shr.sh recipes.

Fixed a real bug hit while proving this: `N=$(... | grep -c NEEDED)`
aborts under `set -e` in the CI job's alpine /bin/sh whenever the DT_NEEDED
count is legitimately zero (grep -c exits 1 on no match) -- present in
both the pre-existing aarch64 job and the new x86_64 one; both fixed.

Ground-truth proof, real alpine:3.20 musl containers (arm64 emulated,
amd64 native), `cmake --build . --target link_native_graph`:
  - x86_64:  9/9 artifacts EM_X86_64, zero DT_NEEDED, via LINK.EXE
  - aarch64: 9/9 artifacts EM_AARCH64, zero DT_NEEDED (regression, unchanged)

Adds CI job link-native-cmake-x86_64 alongside link-native-cmake,
mirroring dcl-native-x86_64's amd64-native (no QEMU) approach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-6da: extend the VMS-native LINK.EXE CMake graph to x86_64

Supersedes work/vms-b6a (discarded per operator ruling): vms-6da's
implementer independently hit the same lib_build_graph.sh conflict
(ARCH/CFLAGS convention vs the inline LIBVMSSYS$SHR recipe), merged
work/vms-b6a in, and reconciled it correctly -- keeping the deduped
mk_vmssys_shr.sh consistent with the ARCH/CFLAGS convention already
integrated via vms-cb5f/vms-a66. Clean automatic merge here, no
conflicts.
…NK.EXE

Replaces vms-913.6 (cancelled -- DT_HASH/ld-based dynamic ELF was a proven
dead end). distro/Dockerfile.bootable now builds a real alpine:3.20 musl
link-native stage that runs `cmake --build --target link_native_graph`
(OVMX_LINK_NATIVE, vms-b6a/vms-6da) to produce the 7 shareables
(DECC$SHR, LIBVMSSYS$SHR, LIBVMSPROCESS$SHR, LIBVMSLNM$SHR, LIBVMSFS$SHR,
LIBVMS$SHR, LIBVMSRMS$SHR) + DCL.EXE + LOGINOUT.EXE via LINK.EXE, plus
IMGACT.EXE (its own standalone Makefile, x86_64). All 9 artifacts are
ET_DYN with a .vms$sv symbol vector, zero DT_NEEDED/DT_HASH -- ground-
truth readelf assertions are baked into the Docker build itself (both in
the link-native stage and against the actually-shipped DCL.EXE in the fat
initramfs), not just asserted by a separate CI job. STARTUP.EXE and
IMGACT.EXE stay static/freestanding; HELP/AUTHORIZE/MAIL/MONITOR/
INITIALIZE ship static for 0.1 (no mk_*.sh recipe yet, scope decision --
DCL is the flagship dynamic proof, not every utility).

Ground-truth verified locally: full `docker build` succeeds, QEMU x86_64
boot reaches login (tests/uat/vms_session_qemu.sh: SYSTEM and GUEST
sessions authenticate, DCL runs 50+ commands to VMS-correct output,
DIRECTORY SYS$SYSTEM: lists the new DCL.EXE/IMGACT.EXE/LOGINOUT.EXE),
and test_executive_integral.sh's negative controls (NOEXEC/NODEV,
rebuilt from the same fat initramfs) still pass 14/14.

KNOWN REGRESSION, not fixed here (out of this item's file-domain --
distro/Dockerfile.bootable + CMake wiring, not src/vmsdcl or
src/kernel/vmsfs): SPAWN's first invocation in a session now fails
(%DCL-E-CREPRC) where it previously succeeded. Root-caused via an A/B
rebuild swapping only DCL.EXE/LOGINOUT.EXE back to static (54/54 UAT
checks pass) vs the VMS-native pair (52/54, this SPAWN check newly red).
cmd_spawn() (src/vmsdcl/dcl_cmd_process.c) re-execs via
readlink("/proc/self/exe"), which resolves to a path vmsfs reports
"(deleted)" -- confirmed present already at the top of DCL's own main(),
before any DCL code runs, so the deletion happens during kernel PT_INTERP
+ IMGACT activation, not in DCL or LOGINOUT. No unlink()/rename() of
SYSEXE exists in the userspace boot path (checked ovmx_init.c,
vms_login.c, vmsfs_translate.c) -- the leading hypothesis is a vmsfs.ko
dentry-lifecycle interaction exposed by IMGACT's longer activation time,
which needs its own investigation. Login and the item's own done
condition are unaffected; test file left unmodified (weakening an
existing UAT assertion is outside this item's authority).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ROOT CAUSE. vmsfs.ko's ->d_revalidate answered "invalid" for EVERY
positive regular-file dentry, unconditionally. That is not revalidation,
it is permanent invalidation: a d_revalidate() of 0 makes the VFS call
d_invalidate(), which UNHASHES the dentry (fs/namei.c lookup_fast() /
lookup_open()); an unhashed non-root dentry satisfies d_unlinked(); and
d_path() renders any d_unlinked() path with a " (deleted)" suffix
(fs/d_path.c path_with_deleted()). /proc/<pid>/exe and /proc/<pid>/fd/<n>
are d_path() readers.

So the FIRST path walk of a running executable that lives on vmsfs made
that program's own /proc/self/exe read ".../DCL.EXE (deleted)" -- with
the file present and unmodified. mm->exe_file pins the dentry, so it
stayed unhashed for the life of the process. DCL's SPAWN re-execs itself
via readlink("/proc/self/exe") (cmd_spawn(), src/vmsdcl/dcl_cmd_process.c),
so it execl()'d a path with " (deleted)" on the end, got ENOENT, and
answered %DCL-E-CREPRC.

That single defect explains BOTH observed shapes, which differ only in
when the second walk happens:
  - static DCL.EXE: nothing re-walks the image during startup, so the
    first SPAWN's own execl() was the second walk -- spawn #1 worked,
    spawn #2 onward failed. (The "SECOND SPAWN fails" defect recorded in
    tests/uat/vms_session_qemu.sh, previously blamed on DCL.)
  - VMS-native, IMGACT-activated DCL.EXE (vms-d0f5e): IMGACT.EXE re-opens
    the image by AT_EXECFN to read its .vms$sv/.vms$imp sections
    (activate_symbol_vector(), src/imgact/imgact.c) BEFORE the image runs,
    so the dentry was already unhashed at the first line of main() and the
    FIRST SPAWN failed. IMGACT did not cause the bug; it reached it one
    walk earlier.

Nothing ever unlinked or renamed anything, which is why the userspace
audit of ovmx_init.c / vms_login.c / vmsfs_translate.c found nothing.

FIX. ->d_revalidate now asks the resolver the question a fresh ->lookup
would ask -- "what does this name resolve to right now?" -- and keeps the
dentry when the answer is unchanged:
  - block-device mode: re-resolve the name to a FID (vmsfs_blkdev_resolve(),
    factored out of vmsfs_blkdev_lookup() with no behaviour change) and
    compare against i_ino, which IS the FID (iget_locked(sb, fid)). Exact
    identity: catches a newer version AND a deletion.
  - overlay mode: compare the current highest version of the base name
    against the version this dentry resolved to (overlay mints a fresh
    inode per lookup, so version is the only stable identity).
  - create intent (LOOKUP_CREATE/LOOKUP_RENAME_TARGET) still returns 0, so
    O_CREAT cannot be satisfied from the cache and VMS still cuts a NEW
    VERSION rather than reopening the current one.
This is also strictly cheaper than the old behaviour, which paid for the
same resolution and then threw away the dentry and the inode anyway.

GROUND SOURCE, BOTH DIRECTIONS, ON THE REAL RUNTIME (Rule 6 -- no module
was loaded on the host; everything below ran under QEMU).

New suite tests/qemu/test_kmod_vmsfs_exepath.c, 28 assertions:
  pre-fix  3 phases red, incl. "child: /proc/self/exe after re-open =
           /mnt/.../CHILD.EXE (deleted)" -- the product symptom reproduced
           at the kernel layer -- then the kernel OOPSES in __fput()
           (NULL d_inode) when the held fd is closed.
  post-fix 28/28 green.
Phase 3 execs this program from a real block-device vmsfs and has the
child do exactly what IMGACT does (re-open its own image by path) and
then what SPAWN does (re-exec via /proc/self/exe). Phase order puts it
first precisely so it is REACHED before the pre-fix oops kills the
process. Phase 2 carries the POSITIVE CONTROL that makes the file
non-vacuous: once PROBE.TXT;2 exists, the fd held on ;1 MUST become
"(deleted)", because the unversioned name no longer names it -- a
d_revalidate that just answers "valid" passes everything else here and
goes red on that one.

tests/uat/vms_session_qemu.sh on the vms-d0f5e VMS-native fat initramfs:
  pre-fix  52/54 (both SPAWN assertions red)
  post-fix 54/54
A SECOND spawn ('SPAWN SHOW TIME') and two assertions on it are added
here, because the long-standing "second SPAWN" defect is fixed by
the same change -- measured, not assumed. With them: 52/56 pre-fix,
56/56 post-fix. The comment block that recorded that defect as a
DCL bug is corrected in place rather than removed.

No regression: full QEMU kernel harness 31/31 suites (incl. the existing
test_kmod_vmsfs and test_kmod_vmsfs_blkdev version-semantics suites),
test_persistent_boot.sh 14/14, test_executive_integral.sh 14/14, Rule 9
runtime-target gate, divider integrity, harness verdict, kif caller
census, identity census, facility manifest selftest+coverage.

tests/qemu/facility_defects.sh: the new suite joins the two existing
vmsfs suites in SCOPE_OUT_SUITES for the reason already stated there --
it never opens /dev/vms, so no executive mutation can turn it red.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-00e: vmsfs must not unhash the dentry of a running image (SPAWN fix)

Contains work/vms-d0f5e (fat initramfs ships DCL.EXE/LOGINOUT.EXE
VMS-native via LINK.EXE, which itself includes vms-6da/vms-b6a) --
confirmed via git merge-base --is-ancestor that vms-d0f5e is fully
contained in vms-00e's history, so vms-d0f5e is not merged separately.

Clean automatic merge, no conflicts.
…ge DB

Checked current state first (per item instructions): vms-p78.3's install
unification and vms-d0f5e's real-file fat initramfs already held, and
test_persistent_boot.sh was passing -- but done condition #4 (SYSTARTUP_VMS.COM
carrying INSTALL ADD for each shareable) was not met, and fixing it surfaced
two real, ground-verified gaps beneath it:

1. DCL's INSTALL builtin (cmd_install) never dispatched to SYS$SYSTEM:INSTALL.EXE
   (src/install/install.c, vms-913.5's KFE-database utility). It reimplemented
   its own flat-text SYS$MANAGER:INSTALL_LIST.DAT that nothing ever read --
   IMGACT.EXE's known-image search (src/imgact/known_images.c) mmaps the binary
   VMS$KNOWN_IMAGES.DAT only INSTALL.EXE writes. Fixed by making cmd_install a
   thin wrapper that re-execs INSTALL.EXE via dcl_exec_utility(), the same
   pattern already used for ANALYZE/MAIL/SYSGEN/SYSMAN in this file -- matching
   install.c's own header comment ("deliberately NOT wired as a DCL builtin").

2. Once wired, real QEMU boot (docker build + test_persistent_boot.sh) caught
   that INSTALL.EXE was never copied into the fat initramfs, so every
   SYSTARTUP_VMS.COM INSTALL ADD failed with %INSTALL-F-NOIMG on live boot --
   and that failure silently aborted the rest of SYSTARTUP_VMS.COM despite
   SET NOON, dropping the "site startup ran" banner test_persistent_boot.sh
   checks for (14/14 -> 12/14). Fixed by adding INSTALL.EXE to
   Dockerfile.bootable's fat-initramfs SYSEXE copy list, alongside the other
   build-static utilities.

SYSTARTUP_VMS.COM now INSTALL ADDs exactly the 7 shareables the fat initramfs
actually ships (DECC$SHR, LIBVMSSYS$SHR, LIBVMS$SHR, LIBVMSPROCESS$SHR,
LIBVMSLNM$SHR, LIBVMSFS$SHR, LIBVMSRMS$SHR -- Dockerfile.bootable's own "9
VMS-native LINK.EXE artifacts" gate). LIBVMSQUEUE$SHR is deliberately excluded:
it builds via the ordinary CMake add_library() graph, not the VMS-native
LINK.EXE graph, and is not shipped in the fat initramfs -- INSTALLing it would
FILNOTFND on every boot.

Verified live: docker build -f distro/Dockerfile.bootable + test_persistent_boot.sh,
14/14 checks pass across both boots, with all 7 INSTALL-I-ADDED lines visible
each time (idempotent re-add on reboot, matching real VMS SYSTARTUP_VMS.COM
practice).

New test: tests/dcl/test_install_command.sh proves through real vmsdcl (not
around it) that INSTALL ADD/LIST/REMOVE write and clear the actual KFE binary
database (magic-byte check), not just matching text output -- the old stub
could print the same %INSTALL-I-ADDED text without touching the file IMGACT.EXE
reads.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-913.7: wire SYSTARTUP_VMS.COM's INSTALL ADD to the real Known Image DB

SCHEMA CHANGE: DCL's INSTALL builtin now writes a binary KFE database
(VMS$KNOWN_IMAGES.DAT) instead of the old dead text format. Nothing
else merged so far touches INSTALL.EXE's database format.

Clean automatic merge, no conflicts.
…CI comments

Live-verified on this worktree's native x86_64 host: docker build
distro/Dockerfile.bootable + tests/uat/vms_session_qemu.sh boots real
QEMU (qemu-system-x86_64, no emulation) through IMGACT.EXE's x86_64
relocation path (RELATIVE/GLOB_DAT/JUMP_SLOT/TLSDESC) to a DCL login
prompt, drives a full scripted session, 56/56 checks passed. This is
already the uat-session CI job (Job 7), unconditional on every push
since GH runners are x86_64 natively.

Job 8/9 comments still said "aarch64-only until bead vms-913.11" as if
x86_64 support were still pending; it has its own native-runner
counterpart (Job 9b, imgact-x86_64) and the uat-session boot-to-login
proof. Corrected the comments to point at the now-complete state
instead of a stale forward reference.

Done condition satisfied by existing work (vms-8f5, vms-cd1, vms-2e4,
vms-a66, vms-00e, vms-d0f5e chain); no functional code change needed.
…, not just DCL.EXE

vms-c39's done condition (STARTUP execs VMS-native LOGINOUT.EXE, which
authenticates against SYSUAF and execs VMS-native DCL.EXE, zero ld/ld.so)
was structurally unverifiable in the Docker CI container -- no /dev/vms
there. This item's job is proving the SUCCESSFUL leg under a real kernel.

Verified live, not assumed: that proof already exists and is repeatable.
- src/ovmx_init/ovmx_init.c execl()s SYS$SYSTEM:LOGINOUT.EXE.
- tools/vms_login.c (LOGINOUT) authenticates against SYSUAF
  (sysuaf_authenticate), then execl()s DCL.EXE --login.
- src/vmslink/link.c sets PT_INTERP=IMGACT.EXE on every LINK.EXE
  executable image -- ground truth that no ld.so is anywhere in this
  chain, not an inference.
- distro/Dockerfile.bootable's fat initramfs ships exactly one DCL.EXE
  and one LOGINOUT.EXE, both copied only from the VMS-native
  /link-native build (no static fallback exists for either anymore).
- tests/uat/vms_session_qemu.sh runs this exact chain under real QEMU
  in CI (job uat-session, .github/workflows/ci.yml), and vms-00e's own
  commit records a fresh 56/56 pass on this initramfs today.

The one real gap: the Dockerfile's own ground-source readelf gate (does
the SHIPPED image -- the actual bytes cp'd into the initramfs, not a
copy two directories away -- carry zero DT_NEEDED/DT_HASH) only checked
DCL.EXE. LOGINOUT.EXE is the FIRST VMS-native image in the login chain
and had no equivalent check on its shipped bytes; a regression that
silently reintroduced an ld-linked LOGINOUT.EXE ahead of DCL.EXE would
not have been caught by this gate (the generic 9-artifact loop in the
earlier link-native stage checks a build-output copy, not what actually
ships). Fixed by widening the existing gate to loop over both images.

Ground-truth verified locally: `docker build -f distro/Dockerfile.bootable
--target builder` -- real build, not mocked -- prints
"OK: shipped DCL.EXE is VMS-native (EM_X86_64, zero DT_NEEDED/DT_HASH)"
and "OK: shipped LOGINOUT.EXE is VMS-native (EM_X86_64, zero
DT_NEEDED/DT_HASH)" for the actual artifacts this build produced. The
new for-loop's shell logic was also unit-checked standalone: fails
correctly against a real dynamically-linked ELF (DT_NEEDED present) and
passes correctly against a statically-linked one, before spending a
build cycle on it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-913.11: verify x86_64 boot-to-login is already proven, fix stale CI comments

CI comment fix only, no functional change. Clean automatic merge.
vms-fbc: shipped-image ground-source gate now covers LOGINOUT.EXE too, not just DCL.EXE

Widened a Dockerfile readelf gate; no runtime behavior change. Clean
automatic merge (no conflict with vms-913.11's disjoint ci.yml change
or vms-913.7's single-line Dockerfile.bootable addition).
The slim initramfs (STARTUP.EXE-only) and boot.sh --slim wiring already
existed (Dockerfile.bootable, boot.sh) but nothing exercised the boot
path: test_persistent_boot.sh's two boots both used the FAT initramfs.

Extends that harness with:
  - a static check that the slim initramfs cpio listing carries no
    DCL.EXE/LOGINOUT.EXE/IMGACT.EXE/SYSLIB (bootstrap-only, as designed)
  - Boot 3: boots the SAME installed disk with the SLIM initramfs, logs
    in as SYSTEM/MANAGER over the QEMU serial console (real SHA256-backed
    SYSUAF credentials, same as tests/uat/vms_session_qemu.sh), and runs
    SHOW TIME to a real DCL prompt

Since the slim initramfs structurally ships none of LOGINOUT.EXE,
IMGACT.EXE, DCL.EXE, or the SYSLIB shareables, a real login reaching a
working DCL prompt is functional proof they resolve from the mounted
system disk's SYS$SYSTEM:/SYS$LIBRARY:, not the initramfs. Measured
against a real QEMU boot: 25/25 checks pass.

Also adds `cpio` to the runner image's apt install list -- needed by the
new static check, absent from the base ubuntu:24.04 image (verified).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-913.10: prove slim-boot login+DCL activates from disk, not initramfs

Adds a new Boot 3 (slim-initramfs login proof) to
tests/qemu/test_persistent_boot.sh, plus the cpio apt-get dependency
in the Dockerfile runner stage. Clean automatic merge despite three
branches (vms-913.7, vms-fbc, vms-913.10) all touching
distro/Dockerfile.bootable -- disjoint hunks, no conflict markers.
Light release-eng doc scoped to 0.1 (download/build the fat-initramfs
image, first-boot install, reboot into the slim initramfs, log in to
DCL). Cluster admin / license audit / trademark review stay under
vms-d5b R6 for 1.0.

Ground-sourced: a real `docker build -f distro/Dockerfile.bootable -t
ovmx-boot .` was run on this checkout and reached the builder stage
before this shared host's disk filled and the build was aborted for
safety. Every command and every piece of documented console output
(the %STARTUP-I-* banners, %OVMX-I-EXEC, Username:/Password: prompts,
Welcome to OVMX, SHOW TIME) is instead quoted verbatim from GitHub
Actions run 31128513528 (commit 8560fa7), where the "Persistent Boot
Smoke Test" and "VMS User Acceptance Test" CI jobs build and boot this
same image and passed 14/14 + all UAT assertions -- the proven,
passing path tests/qemu/test_persistent_boot.sh and
tests/uat/vms_session_qemu.sh already exercise on every push. Login
credential (SYSTEM/MANAGER) confirmed against the real hash in
distro/rootfs/.../SYSUAF.DAT, not invented.

No git tag created -- that step is reserved for the operator's final
sign-off.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-0c9: docs/install-0.1.md -- 0.1 install/boot/login walkthrough

Docs-only, no code. Clean automatic merge. Last item in the vms-b93
closure.
…ng note

Run 31128513528 predates the slim-boot work (checked-out
test_persistent_boot.sh has zero slim references) and its overall
status was FAILURE (Build & Test + attribution negative-control jobs
red); only citing the two individually-passing jobs overclaimed
coverage. Rescope the note to state what was actually verified: this
swarm's own local docker build + real QEMU boots against the merged
tree (25/25 checks) for Section 3, and the UAT script's own run for
the DCL session content. No CI run number is cited as covering
slim-boot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s-913.7

Re-ran tools/survey_x86_64_relocs.sh to refresh the empirical R_X86_64_PC32/
PLT32 counts in src/libvms/descrip.c, no methodology change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vms-9bc: regenerate docs/design-link-x86_64-relocs.md, stale after vms-913.7

Post-merge veracity audit fix: ctest x86_64_reloc_survey_fresh was red
because the doc was stale. Clean automatic merge.
vms-ade8: fix overclaimed CI citation in install-0.1.md ground-sourcing note

Post-merge veracity audit fix. Clean automatic merge.
Comment thread .github/workflows/ci.yml
Comment on lines +1147 to +1195
name: VMS-native LINK.EXE Graph via CMake (OVMX_LINK_NATIVE)
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Register QEMU binfmt handlers (arm64 emulation)
uses: docker/setup-qemu-action@v3
with:
platforms: arm64

- name: cmake -B build && cmake --build build (arm64 musl) -- assert the shipped graph is LINK.EXE-native
run: |
docker run --rm --platform linux/arm64 \
-v "${{ github.workspace }}":/src -w /src \
docker.io/library/alpine:3.20 sh -c '
set -e
apk add --no-cache cmake gcc g++ musl-dev binutils make linux-headers >/dev/null
cmake -B /tmp/build -DBUILD_TESTS=OFF -DBUILD_TOOLS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build /tmp/build --target link_native_graph -j"$(nproc)"
echo "== readelf: EM_AARCH64, zero DT_NEEDED, across all 9 artifacts =="
SYSLIB=/tmp/build/link-native/SYSLIB
SYSEXE=/tmp/build/link-native/SYSEXE
for f in "$SYSLIB"/*.EXE "$SYSEXE"/*.EXE; do
echo "-- $f --"
readelf -h "$f" | grep -q "Machine:.*AArch64" || { echo "FAIL: $f not EM_AARCH64"; exit 1; }
N=$(readelf -d "$f" 2>/dev/null | grep -c NEEDED || true)
[ "$N" -eq 0 ] || { echo "FAIL: $f has $N DT_NEEDED entries (ld-linked, not VMS-native)"; exit 1; }
done
COUNT=$(ls "$SYSLIB"/*.EXE "$SYSEXE"/*.EXE | wc -l)
[ "$COUNT" -eq 9 ] || { echo "FAIL: expected 9 artifacts (5 libs + DECC\$SHR + DCL.EXE + LOGINOUT.EXE), got $COUNT"; exit 1; }
echo "OK: $COUNT VMS-native LINK.EXE artifacts, all EM_AARCH64, zero DT_NEEDED"
'

# -----------------------------------------------------------------------
# link-native-cmake-x86_64: the x86_64 analog of link-native-cmake above
# (bead vms-6da, epic vms-b93 "0.1 pre-release" -- the direct blocker of
# vms-d0f5e's fat initramfs). Same CMakeLists.txt/build_link_native.sh
# mechanism, ARCH auto-detected from the compiler triple (aarch64-musl vs
# x86_64-musl) -- NOT a duplicated custom target. x86_64 is the project's
# primary architecture (CLAUDE.md Rule 5) and this is what a shipped fat
# initramfs actually needs. Asserts, via readelf, that all 9 artifacts are
# EM_X86_64 with zero DT_NEEDED entries. Unlike the arm64 job, GitHub's
# runner IS x86_64 (see dcl-native-x86_64 above), so this needs no QEMU
# binfmt registration -- the alpine:3.20/amd64 container runs natively.
# -----------------------------------------------------------------------
link-native-cmake-x86_64:
Comment thread .github/workflows/ci.yml
Comment on lines +1574 to +1606
sh src/imgact/test/run_multiobj_exec_x86_64.sh

# -----------------------------------------------------------------------
# Job: Read-only-section relocations — x86_64 (vms-a66)
#
# LINK.EXE used to collect relocations ONLY against sections it bucketed
# B_TEXT or B_DATA, silently discarding every relocation whose target was a
# read-only allocatable section. gcc emits each switch jump table into
# `.rodata` as `.long arm - table_base` (one R_X86_64_PC32 per arm, because
# the arms are in .text and the table is not), so those tables came out ALL
# ZERO and the dispatch `jmp *rdx` executed the table's own bytes. That is
# what made DCL.EXE segfault on x86_64: musl's pop_arg jumped into the
# "(null)" string in DECC$SHR's .rodata on the first %-conversion.
#
# The three jobs that "covered" this path before (multiobj-exec-x86_64,
# vmslink-x86_64, exec-tls) all passed through the bug because their
# specimens contained no switch big enough for a jump table and never
# executed one. This job reproduces the SHAPE directly — it is not a
# scale/import-count gate — and diffs the activated image's output against
# the same source built by the system toolchain.
# -----------------------------------------------------------------------
rodata-reloc-x86_64:
name: Read-only-section relocations (switch jump tables) — x86_64 (vms-a66)
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Link + activate a jump-table-bearing image against real musl DECC$SHR (x86_64)
run: |
docker run --rm --platform linux/amd64 \
/* Two different kinds of second walk: stat(), then open(). */
if (stat(path, &st) != 0)
break;
fd2 = open(path, O_RDONLY);
@baron-3dl
baron-3dl merged commit e6bc268 into main Aug 7, 2026
38 of 43 checks passed
@baron-3dl
baron-3dl deleted the work/vms-b93-integration branch August 7, 2026 02:00
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.

2 participants