From f3bd8a072d0e3b43e738b8d3624b34daec24a033 Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 17 Aug 2026 03:09:09 +0000 Subject: [PATCH 001/106] vms-5303: ACP answers IO$_CREATE / IO$_DELETE / IO$_MODIFY (file header alloc + dir insert + dealloc, epic vms-208) The sixth and final rung of the executive Files-11 ODS-2 ACP-QIO surface: the ACP now creates, deletes and modifies files against a real /dev/vms. Codec port (Rule 8 -- ports the proven format logic from the userspace writer, does not invent a new layout; parallel pure-function set in ods2_edit.c): - ods2_fh2_build(): the pure write-side twin of ods2_writer.c's write_fh2_header_ext() -- allocate/init a complete FH2 into a caller block, owner/prot as parameters (INV-6 creator UIC). - ods2_dir_insert_blocks() / ods2_dir_remove_blocks(): the pure twins of ods2_wvolume_dir_insert() / merge_dir_record() -- flatten, splice/merge a versioned {name,version,fid} record (or drop one), greedy-repack into blocks. - ods2_ifbm_block_fid_used/alloc/free(): the index-file (INDEXF.SYS) bitmap bit ops (SET == IN USE, opposite sense from the storage bitmap). The userspace ods2_writer.c path is untouched. ioctl-mapping decision: the ACP band 0x68-0x6F is full and 0x70 is mailboxes, so IO$_CREATE/DELETE/MODIFY route through ONE new func-dispatched ioctl, VMS_IOCTL_ACP_FILEOP, whose `func` field carries the $QIO function code. This is more VMS-faithful than one-ioctl-per-function ($QIO is a single service selected by function code) and extends #641's 0x6F umbrella. FILEOP reuses nr 0x6F with its own larger, ATR-carrying struct: _IOWR folds sizeof into the request number so FILEOP (252 B) and ACPCONTROL (200 B) are distinct 32-bit commands. No ABI break to the frozen ACPCONTROL struct; a _Static_assert guards their distinctness. Handler (vms_ioctl_acp_fileop, gated OVMX_ODS2_KERNEL; codec-free build refuses SS$_DEVNOTMOUNT): CREATE allocates a real FID from the index bitmap, inits the FH2 from the ATR list, optionally extends, enters a new highest version in the directory, optionally accesses; DELETE removes the directory entry and (M_DELETE) deallocates header + blocks; MODIFY extends / truncates (freeing blocks) / writes attributes. Protection-gated (INV-6); fail-honest (SS$_NOSUCHFILE, SS$_BADPARAM, SS$_DEVICEFULL, SS$_DUPLNAM). Proof (real /dev/vms, QEMU kernel-executive harness): test_syssvc_acp_create.c, 27/27 -- CREATE assigns a real FID + ;1 entry, readable back by name; write + persist across DEACCESS/re-ACCESS (INV-6); second create -> ;2 distinct FID; DELETE removes + deallocs (ACCESS -> SS$_NOSUCHFILE); MODIFY extend/truncate/attr each persist; fail-honest edges. Whole harness 89 suites / 1606 assertions, 0 fail. Cascade: new executive symbol vms_kif_acp_fileop appended to libvmssys_shr.vec; genuine negctl anchor acp-create-header-slot-offbyone (INDEXF header-slot off-by-one) in facility_defects.sh (coverage PASS, FLOOR-NO-BUMP: 112 >= 104); vms.ko builds out-of-tree AND codec-free (bootable overlay); kernel-core stays Alpha/VAX-portable (fixed-width types, byte-wise LE accessors). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/kernel-core/vmsfs_acp.c | 744 ++++++++++++++++++++++++++++ src/kernel/vms_acp.h | 125 +++++ src/kernel/vms_internal.h | 7 + src/kernel/vms_module.c | 2 + src/libvmssys/vms_kif.c | 15 + src/libvmssys/vms_kif.h | 12 + src/vmsfs/include/vmsfs/ods2.h | 62 +++ src/vmsfs/ods2/ods2_edit.c | 522 +++++++++++++++++++ src/vmslink/libvmssys_shr.vec | 8 + tests/qemu/facility_defects.sh | 46 ++ tests/qemu/test_syssvc_acp_create.c | 332 +++++++++++++ 11 files changed, 1875 insertions(+) create mode 100644 tests/qemu/test_syssvc_acp_create.c diff --git a/src/kernel-core/vmsfs_acp.c b/src/kernel-core/vmsfs_acp.c index ae799d8df..4f62714f5 100644 --- a/src/kernel-core/vmsfs_acp.c +++ b/src/kernel-core/vmsfs_acp.c @@ -95,6 +95,15 @@ struct vms_acp_volume { * block so IO$_ACCESS need not re-read + re-parse the home block per open. */ uint32_t idx_lbn; /* LBN of INDEXF.SYS file-1 header */ + /* + * Index-file bitmap base + capacity (vms-5303): hm2_ibmaplbn is the LBN of + * the index bitmap (which precedes the headers -- idx_lbn = ibmap_lbn + + * ibmapsize), so file number N's IN-USE bit is bit (N-1) of that bitmap. + * maxfiles bounds a free-FID scan. Both captured at $MOUNT from the home + * block so IO$_CREATE/IO$_DELETE need not re-read it. + */ + uint32_t ibmap_lbn; /* hm2_ibmaplbn: index-bitmap base LBN */ + uint32_t maxfiles; /* hm2_maxfiles: index-file capacity */ }; /* @@ -393,6 +402,8 @@ static uint32_t acp_validate_ods2(uint32_t major, uint32_t minor, out->struclev = s->home.hm2_struclev; out->volsize = s->scb.scb_volsize; out->idx_lbn = idx_lbn; /* INDEXF.SYS file-1 header base (vms-204) */ + out->ibmap_lbn = s->home.hm2_ibmaplbn; /* index-bitmap base (vms-5303) */ + out->maxfiles = s->home.hm2_maxfiles; /* index-file capacity (vms-5303) */ memcpy(out->volname, s->home.hm2_volname, 12); out->volname[12] = '\0'; result = SS__NORMAL; @@ -2142,6 +2153,739 @@ long vms_ioctl_acp_writevb(struct vms_proc *proc, unsigned long arg) return 0; } +/* ================================================================ + * IO$_CREATE / IO$_DELETE / IO$_MODIFY (vms-5303, epic vms-208) -- the ACP + * file-operation umbrella: create a file header (allocate a real FID from + * INDEXF.SYS's index bitmap, init the FH2 from the ATR list, optionally enter + * it in a directory at a new highest version, optionally access it); delete a + * file (remove its directory entry and/or deallocate header + blocks); modify a + * file (extend/truncate/write attributes). See vms_acp.h for the func-dispatch + * ioctl-mapping decision and the FIB/ATR interface (OVMX-labelled, Rule 8 D2). + * + * Every on-disk ODS-2 FORMAT fact is the codec's (ods2_edit.c's pure + * ods2_fh2_build / ods2_dir_insert_blocks / ods2_dir_remove_blocks / index- and + * storage-bitmap bit ops); this file SEQUENCES the raw exec_blockdev_* block + * reads/writes around them, exactly as IO$_ACCESS/IO$_WRITEVBLK do. Gated on + * OVMX_ODS2_KERNEL: without the codec nothing is mountable, so a codec-free + * build fail-honestly refuses (SS$_DEVNOTMOUNT), never a fabricated create. + * ================================================================ */ +#if defined(OVMX_ODS2_KERNEL) + +/* + * Directory-file work bound: the ACP reads the whole directory into a heap + * buffer to insert/remove a record. A directory of more than this many blocks + * is refused fail-honest (SS$_DEVICEFULL) rather than truncated -- generous for + * any directory the boot corpus builds. + */ +#define ACP_DIR_MAX_BLOCKS 64u + +/* Heap scratch for one IO$_CREATE/DELETE/MODIFY (~5 KB, too much for the kernel + * stack). Carries the reused acp_rw_scratch (for acp_bitmap_alloc's bitmap + * RMW) + a snap it fills from the volume, plus the directory/file header blocks + * and a generic bitmap-RMW block. */ +struct acp_fileop_scratch { + struct acp_chan_snap snap; /* filled from the volume for acp_bitmap_alloc */ + struct acp_rw_scratch rw; /* bmhdr/bmblk/blk/hdr for acp_bitmap_alloc */ + uint8_t dirhdr[ACP_BLOCK_SIZE]; + uint8_t filehdr[ACP_BLOCK_SIZE]; + uint8_t ibblk[ACP_BLOCK_SIZE]; /* index-bitmap block RMW */ + ods2_fh2_t dfh; + ods2_fh2_t fh; +}; + +/* Little-endian 16-bit store into a raw FH2 field (fileprot/fileowner edits). + * The on-disk field is byte-authentic; only this scalar write is host-neutral. */ +static void acp_put16(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)(v & 0xFFu); + p[1] = (uint8_t)((v >> 8) & 0xFFu); +} + +/* Fill a channel-snapshot-shaped view from the volume, for acp_bitmap_alloc. */ +static void acp_snap_from_vol(struct acp_chan_snap *snap, struct vms_acp_volume *vol) +{ + memset(snap, 0, sizeof(*snap)); + snap->backing_major = vol->backing_major; + snap->backing_minor = vol->backing_minor; + snap->idx_lbn = vol->idx_lbn; + snap->volsize = vol->volsize; +} + +/* + * acp_fid_alloc - claim the first free file number > ODS2_RESFILES from the + * volume's index bitmap (INDEXF.SYS index bitmap at vol->ibmap_lbn), mark it + * IN USE on disk, and return it. SS$_DEVICEFULL when the index file is full. + * Reserved FIDs (1..ODS2_RESFILES) are never handed out. + */ +static uint32_t acp_fid_alloc(struct vms_acp_volume *vol, uint8_t *ibblk, + uint32_t *fid_out) +{ + uint32_t fidnum, cur_block = 0xFFFFFFFFu, cur_lbn = 0; + + if (vol->maxfiles == 0 || vol->ibmap_lbn == 0) + return SS__DEVNOTMOUNT; + + for (fidnum = ODS2_RESFILES + 1u; fidnum <= vol->maxfiles; fidnum++) { + uint32_t bit = fidnum - 1u; + uint32_t blkidx = bit / ODS2_SBM_BITS_PER_BLOCK; + uint32_t lbn = vol->ibmap_lbn + blkidx; + + if (blkidx != cur_block) { + if (exec_blockdev_read_block(vol->backing_major, vol->backing_minor, + lbn, ibblk, ACP_BLOCK_SIZE) != 0) + return SS__DEVNOTMOUNT; + cur_block = blkidx; + cur_lbn = lbn; + } + if (!ods2_ifbm_block_fid_used(ibblk, bit % ODS2_SBM_BITS_PER_BLOCK)) { + ods2_ifbm_block_alloc(ibblk, bit % ODS2_SBM_BITS_PER_BLOCK); + if (exec_blockdev_write_block(vol->backing_major, vol->backing_minor, + cur_lbn, ibblk, ACP_BLOCK_SIZE) != 0) + return SS__DEVNOTMOUNT; + *fid_out = fidnum; + return SS__NORMAL; + } + } + return SS__DEVICEFULL; /* index file exhausted -- honest */ +} + +/* acp_fid_free - mark file number `fidnum` FREE in the index bitmap (IO$_DELETE + * header deallocation), the inverse of acp_fid_alloc. */ +static uint32_t acp_fid_free(struct vms_acp_volume *vol, uint32_t fidnum, + uint8_t *ibblk) +{ + uint32_t bit = fidnum - 1u; + uint32_t lbn = vol->ibmap_lbn + bit / ODS2_SBM_BITS_PER_BLOCK; + + if (fidnum < 1 || fidnum > vol->maxfiles) + return SS__BADPARAM; + if (exec_blockdev_read_block(vol->backing_major, vol->backing_minor, + lbn, ibblk, ACP_BLOCK_SIZE) != 0) + return SS__DEVNOTMOUNT; + ods2_ifbm_block_free(ibblk, bit % ODS2_SBM_BITS_PER_BLOCK); + if (exec_blockdev_write_block(vol->backing_major, vol->backing_minor, + lbn, ibblk, ACP_BLOCK_SIZE) != 0) + return SS__DEVNOTMOUNT; + return SS__NORMAL; +} + +/* + * acp_free_file_blocks - free every data block in a file's FH2 retrieval map + * back to the volume's storage bitmap (BITMAP.SYS), for IO$_DELETE / a + * MODIFY truncate. Builds the file's extent list and BITMAP.SYS window, then + * RMWs the affected bitmap blocks (SET bit == FREE). `first_freed_vbn` (1-based) + * skips extents wholly before it, so a truncate frees only blocks past the new + * allocation; pass 1 to free the whole file. + */ +static uint32_t acp_free_file_blocks(struct vms_acp_volume *vol, + const uint8_t *filehdr, + struct acp_fileop_scratch *sc, + uint32_t first_freed_vbn) +{ + struct acp_winbuild wb, bwb; + struct acp_win_ext fex[ACP_WINDOW_MAX], bmwin[ACP_WINDOW_MAX]; + uint32_t bmhdr_lbn, e, k, cur_bmvbn = 0xFFFFFFFFu, cur_bmlbn = 0; + int loaded = 0; + + wb.win = fex; wb.max = ACP_WINDOW_MAX; wb.n = 0; wb.next_vbn = 1; wb.overflow = 0; + if (ods2_fh2_map_walk(filehdr, acp_winbuild_cb, &wb, NULL) != ODS2_OK || wb.overflow) + return SS__DEVNOTMOUNT; + + bmhdr_lbn = vol->idx_lbn + (ODS2_FID_BITMAP - 1u); + if (exec_blockdev_read_block(vol->backing_major, vol->backing_minor, + bmhdr_lbn, sc->rw.bmhdr, ACP_BLOCK_SIZE) != 0) + return SS__DEVNOTMOUNT; + bwb.win = bmwin; bwb.max = ACP_WINDOW_MAX; bwb.n = 0; bwb.next_vbn = 1; bwb.overflow = 0; + if (ods2_fh2_map_walk(sc->rw.bmhdr, acp_winbuild_cb, &bwb, NULL) != ODS2_OK || bwb.n == 0) + return SS__DEVNOTMOUNT; + + for (e = 0; e < wb.n; e++) { + for (k = 0; k < fex[e].count; k++) { + uint32_t vbn = fex[e].start_vbn + k; + uint32_t L = fex[e].lbn + k; + uint32_t bmvbn = 2u + L / ODS2_SBM_BITS_PER_BLOCK; + + if (vbn < first_freed_vbn) + continue; /* keep blocks before the truncate point */ + if (!loaded || bmvbn != cur_bmvbn) { + if (loaded && + exec_blockdev_write_block(vol->backing_major, vol->backing_minor, + cur_bmlbn, sc->rw.bmblk, ACP_BLOCK_SIZE) != 0) + return SS__DEVNOTMOUNT; + cur_bmlbn = acp_window_map_vbn(bmwin, bwb.n, bmvbn); + if (cur_bmlbn == 0 || + exec_blockdev_read_block(vol->backing_major, vol->backing_minor, + cur_bmlbn, sc->rw.bmblk, ACP_BLOCK_SIZE) != 0) + return SS__DEVNOTMOUNT; + cur_bmvbn = bmvbn; + loaded = 1; + } + ods2_sbm_block_free(sc->rw.bmblk, L % ODS2_SBM_BITS_PER_BLOCK); + } + } + if (loaded && + exec_blockdev_write_block(vol->backing_major, vol->backing_minor, + cur_bmlbn, sc->rw.bmblk, ACP_BLOCK_SIZE) != 0) + return SS__DEVNOTMOUNT; + return SS__NORMAL; +} + +/* Ordered LBN list of a directory's data blocks, collected VBN-order. */ +struct acp_dir_lbns { + uint32_t lbn[ACP_DIR_MAX_BLOCKS]; + unsigned n; + int overflow; +}; +static int acp_dir_lbn_cb(const ods2_extent_t *ext, void *ctx) +{ + struct acp_dir_lbns *d = (struct acp_dir_lbns *)ctx; + uint32_t k; + for (k = 0; k < ext->count; k++) { + if (d->n >= ACP_DIR_MAX_BLOCKS) { d->overflow = 1; return 1; } + d->lbn[d->n++] = ext->lbn + k; + } + return 0; +} + +/* + * acp_dir_write_grown_map - after an insert grew a directory, rewrite its FH2 + * retrieval map from the full ordered LBN list (coalescing contiguous runs) and + * its recattr end-of-file position, then write the header back. Mirrors + * ods2_writer.c's ods2_wvolume_dir_insert() grown-header rewrite. + */ +static uint32_t acp_dir_write_grown_map(struct vms_acp_volume *vol, + uint8_t *dirhdr, uint32_t dir_hdr_lbn, + const uint32_t *lbns, unsigned nblk) +{ + unsigned mpoff = dirhdr[offsetof(ods2_fh2_t, fh2_mpoffset)]; + uint8_t *mp = dirhdr + (size_t)mpoff * 2u; + size_t mapcap = ACP_BLOCK_SIZE - (size_t)mpoff * 2u - 2u; + unsigned b = 0; + + memset(mp, 0, mapcap); + dirhdr[offsetof(ods2_fh2_t, fh2_map_inuse)] = 0; + while (b < nblk) { + uint32_t run_lbn = lbns[b], run = 1; + while (b + run < nblk && run < 256 && lbns[b + run] == run_lbn + run) + run++; + if (ods2_fh2_map_append(dirhdr, run_lbn, run) != ODS2_OK) + return SS__DEVICEFULL; /* map area full */ + b += run; + } + (void)ods2_fh2_set_eof(dirhdr, nblk, nblk + 1u, 0); /* [F15]/[F17] dir EOF */ + ods2_fh2_reseal(dirhdr); + if (exec_blockdev_write_block(vol->backing_major, vol->backing_minor, + dir_hdr_lbn, dirhdr, ACP_BLOCK_SIZE) != 0) + return SS__DEVNOTMOUNT; + return SS__NORMAL; +} + +/* + * acp_dir_mutate - read a directory's data blocks, apply an insert (op==1) or + * remove (op==0), and write the (possibly grown) result back. Handles growth: + * allocates the extra blocks from BITMAP.SYS and rewrites the directory's FH2 + * map. The dir header block is `dirhdr` (already read + validated by the + * caller); `dir_fidnum` locates its header slot. + */ +static uint32_t acp_dir_mutate(struct vms_acp_volume *vol, + struct acp_fileop_scratch *sc, + uint8_t *dirhdr, uint32_t dir_fidnum, + int op_insert, const char *name, unsigned namecount, + uint16_t version, ods2_fid_t entry_fid, + int is_resfile, int *removed_out) +{ + struct acp_dir_lbns dl; + uint8_t *inbuf = NULL, *outbuf = NULL, *flat = NULL; + unsigned nblk, out_nblk = 0, i; + size_t flat_cap; + uint32_t status = SS__NORMAL; + ods2_status_t st; + + dl.n = 0; dl.overflow = 0; + if (ods2_fh2_map_walk(dirhdr, acp_dir_lbn_cb, &dl, NULL) != ODS2_OK || dl.overflow || + dl.n == 0) + return SS__DEVICEFULL; + nblk = dl.n; + + inbuf = exec_zalloc((size_t)nblk * ACP_BLOCK_SIZE); + outbuf = exec_zalloc((size_t)(nblk + 1u) * ACP_BLOCK_SIZE); + flat_cap = (size_t)(nblk + 1u) * ACP_BLOCK_SIZE + 1024u; + flat = exec_zalloc(flat_cap); + if (!inbuf || !outbuf || !flat) { + status = SS__INSFMEM; + goto done; + } + + for (i = 0; i < nblk; i++) { + if (exec_blockdev_read_block(vol->backing_major, vol->backing_minor, + dl.lbn[i], inbuf + (size_t)i * ACP_BLOCK_SIZE, + ACP_BLOCK_SIZE) != 0) { + status = SS__DEVNOTMOUNT; + goto done; + } + } + + if (op_insert) + st = ods2_dir_insert_blocks(inbuf, nblk, name, namecount, version, entry_fid, + is_resfile, flat, flat_cap, outbuf, nblk + 1u, &out_nblk); + else { + int removed = 0; + st = ods2_dir_remove_blocks(inbuf, nblk, name, namecount, version, + flat, flat_cap, outbuf, nblk + 1u, &out_nblk, &removed); + if (removed_out) + *removed_out = removed; + } + if (st == ODS2_ERR_ARGS) { /* duplicate {name, version} on insert */ + /* Oracle-pinned SS$_DUPLNAM (148, "duplicate name"); the file-specific + * SS$_DUPFILNAM value is not yet lab-pinned, so the general pinned + * duplicate-name status is used rather than an invented constant + * (CLAUDE.md VMS-purity guardrail / empirical-not-gate). */ + status = SS__DUPLNAM; + goto done; + } + if (st != ODS2_OK) { + status = SS__DEVICEFULL; /* map/pack overflow -- honest */ + goto done; + } + + /* Grow the directory file if the repack needs more blocks (insert only). */ + if (out_nblk > nblk) { + uint32_t run_lbn = 0, extra = out_nblk - nblk, k; + acp_snap_from_vol(&sc->snap, vol); + status = acp_bitmap_alloc(&sc->snap, &sc->rw, extra, &run_lbn); + if (status != SS__NORMAL) + goto done; + for (k = 0; k < extra; k++) { + if (dl.n >= ACP_DIR_MAX_BLOCKS) { status = SS__DEVICEFULL; goto done; } + dl.lbn[dl.n++] = run_lbn + k; + } + } + + /* Write the (possibly grown) directory blocks. */ + for (i = 0; i < out_nblk; i++) { + if (exec_blockdev_write_block(vol->backing_major, vol->backing_minor, + dl.lbn[i], outbuf + (size_t)i * ACP_BLOCK_SIZE, + ACP_BLOCK_SIZE) != 0) { + status = SS__DEVNOTMOUNT; + goto done; + } + } + + /* Rewrite the directory's own FH2 map + EOF only when it grew. */ + if (out_nblk > nblk) + status = acp_dir_write_grown_map(vol, dirhdr, + vol->idx_lbn + (dir_fidnum - 1u), + dl.lbn, out_nblk); + +done: + if (inbuf) exec_free(inbuf); + if (outbuf) exec_free(outbuf); + if (flat) exec_free(flat); + return status; +} + +/* Build a channel window from a freshly-created/modified file header, marking + * the channel accessed (IO$_CREATE with IO$M_ACCESS -- the same state IO$_ACCESS + * sets). Caller holds no lock. */ +static uint32_t acp_access_from_header(struct vms_proc *proc, uint32_t chan, + const uint8_t *filehdr, const ods2_fh2_t *fh, + ods2_fid_t fid, uint16_t version, int want_write) +{ + struct acp_winbuild wb; + struct acp_win_ext window[ACP_WINDOW_MAX]; + struct vms_acp_chan *ch; + + wb.win = window; wb.max = ACP_WINDOW_MAX; wb.n = 0; wb.next_vbn = 1; wb.overflow = 0; + if (ods2_fh2_map_walk(filehdr, acp_winbuild_cb, &wb, NULL) != ODS2_OK) + return SS__DEVNOTMOUNT; + if (wb.overflow) + return SS__NOSUCHFILE; + + exec_lock(&proc->chan_lock); + ch = acp_chan_find_locked(proc, chan); + if (!ch) { + exec_unlock(&proc->chan_lock); + return SS__IVCHAN; + } + ch->file_accessed = 1; + ch->acc_write = want_write ? 1 : 0; + ch->acc_version = version; + ch->acc_fid_num = fid.fid_num; + ch->acc_fid_seq = fid.fid_seq; + ch->acc_fid_rvn = fid.fid_rvn; + ch->acc_fid_nmx = fid.fid_nmx; + ch->acc_efblk = ods2_recattr_efblk(&fh->fh2_recattr); + ch->acc_ffbyte = fh->fh2_recattr.fat_ffbyte; + ch->win_n = wb.n; + memcpy(ch->win, window, wb.n * sizeof(window[0])); + exec_unlock(&proc->chan_lock); + return SS__NORMAL; +} + +#endif /* OVMX_ODS2_KERNEL */ + +long vms_ioctl_acp_fileop(struct vms_proc *proc, unsigned long arg) +{ + struct vms_acp_fileop_args args; + struct vms_acp_chan *ch; + struct vms_acp_volume *vol = NULL; + + memset(&args, 0, sizeof(args)); + if (exec_copyin(&args, (const void *)arg, sizeof(args))) + return -EFAULT; + args.name[VMS_ACP_NAME_SIZE - 1] = '\0'; + + if (args.func != VMS_ACP_FOP_CREATE && args.func != VMS_ACP_FOP_DELETE && + args.func != VMS_ACP_FOP_MODIFY) { + args.status = SS__BADPARAM; /* unknown $QIO function code */ + goto out; + } + + exec_lock(&proc->chan_lock); + ch = acp_chan_find_locked(proc, args.chan); + if (ch) + vol = ch->vol; + exec_unlock(&proc->chan_lock); + if (!ch || !vol) { + args.status = SS__IVCHAN; + goto out; + } + +#if defined(OVMX_ODS2_KERNEL) + { + struct acp_fileop_scratch *sc = exec_zalloc(sizeof(*sc)); + uint32_t status; + + if (!sc) + return -ENOMEM; + + if (args.func == VMS_ACP_FOP_CREATE) { + ods2_fid_t did, new_fid, backlink; + uint32_t did_num, new_fidnum = 0, hdr_lbn, alloc_lbn = 0; + uint16_t new_version; + unsigned kind = args.kind; + uint32_t filechar = 0; + ods2_extent_t ext; + unsigned n_ext = 0; + ods2_uic_t owner; + uint16_t fileprot = 0; + int is_dir = (args.attr.filechar & ODS2_FH2_M_DIRECTORY) != 0; + + /* Resolve the directory (for the entry + version selection). */ + memset(&did, 0, sizeof(did)); + did.fid_num = args.did_num; + did.fid_nmx = args.did_nmx; + did_num = ods2_fid_number(&did); + if (did_num == 0) + did_num = ODS2_FID_MFD; + + status = acp_read_header(vol, did_num, sc->dirhdr, &sc->dfh); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + if (!(sc->dfh.fh2_filechar & ODS2_FH2_M_DIRECTORY)) { + args.status = SS__NOSUCHFILE; /* DID is not a directory */ + goto free_sc; + } + /* Writing a directory entry is a write to the directory file. */ + status = acp_check_access(proc, &sc->dfh, 1); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + + /* New highest version (VMS $CREATE default): highest existing + 1. */ + if (args.version != 0) { + new_version = args.version; + } else { + ods2_fid_t cur; uint16_t curver = 0; + uint32_t fst = acp_dir_find(vol, sc->dirhdr, sc->rw.blk, args.name, + 0, &cur, &curver); + new_version = (fst == SS__NORMAL) ? (uint16_t)(curver + 1u) : 1u; + } + + /* Allocate a real FID from the index bitmap. */ + status = acp_fid_alloc(vol, sc->ibblk, &new_fidnum); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + + /* Optional initial allocation (FIB$L_EXSZ). */ + if (args.exsz > 0) { + acp_snap_from_vol(&sc->snap, vol); + status = acp_bitmap_alloc(&sc->snap, &sc->rw, args.exsz, &alloc_lbn); + if (status != SS__NORMAL) { + (void)acp_fid_free(vol, new_fidnum, sc->ibblk); /* roll back the FID */ + args.status = status; + goto free_sc; + } + ext.lbn = alloc_lbn; ext.count = args.exsz; n_ext = 1; + } + + /* Build the FH2. Owner = the creating process's UIC (INV-6). */ + if (is_dir) { + kind = ODS2_FK_DIR; + filechar = ODS2_FH2_M_DIRECTORY | ODS2_FH2_M_CONTIG; + } else if (kind != ODS2_FK_DATA && kind != ODS2_FK_DATA_FIX && + kind != ODS2_FK_SYSTEM) { + kind = ODS2_FK_DATA_FIX; /* default: a fixed-record data file */ + } + owner.uic_group = (uint16_t)((proc->uic >> 16) & 0xFFFFu); + owner.uic_member = (uint16_t)(proc->uic & 0xFFFFu); + if (args.attr_ctl & VMS_ACP_ATTR_OWNER) { + owner.uic_group = args.attr.uic_group; + owner.uic_member = args.attr.uic_member; + } + if (args.attr_ctl & VMS_ACP_ATTR_PROT) + fileprot = args.attr.fileprot; + + memset(&backlink, 0, sizeof(backlink)); + backlink.fid_num = (uint16_t)(did_num & 0xFFFF); + backlink.fid_seq = sc->dfh.fh2_fid.fid_seq; + backlink.fid_nmx = (uint8_t)(did_num >> 16); + if (ods2_fh2_build(sc->filehdr, new_fidnum, 1, args.name, new_version, + filechar, kind, n_ext ? &ext : NULL, n_ext, + 0, backlink, owner, fileprot, vol->maxfiles) != ODS2_OK) { + (void)acp_fid_free(vol, new_fidnum, sc->ibblk); + args.status = SS__BADPARAM; + goto free_sc; + } + /* A preallocated data file is allocated-but-EMPTY: keep hiblk but set + * EOF to the empty position so a later $PUT/WRITEVBLK extends from 0. */ + if (n_ext > 0 && !is_dir) { + (void)ods2_fh2_set_eof(sc->filehdr, args.exsz, 1, 0); + ods2_fh2_reseal(sc->filehdr); + } + + /* Write the header at the FID's INDEXF slot. NEGCTL-ANCHORED: the + * slot LBN is idx_lbn + (new_fidnum - 1u); an off-by-one writes the + * header one slot too high, so the created file is unreadable by its + * own FID. */ + hdr_lbn = vol->idx_lbn + (new_fidnum - 1u); + if (exec_blockdev_write_block(vol->backing_major, vol->backing_minor, + hdr_lbn, sc->filehdr, ACP_BLOCK_SIZE) != 0) { + (void)acp_fid_free(vol, new_fidnum, sc->ibblk); + args.status = SS__DEVNOTMOUNT; + goto free_sc; + } + + new_fid.fid_num = (uint16_t)(new_fidnum & 0xFFFF); + new_fid.fid_seq = 1; + new_fid.fid_rvn = 0; + new_fid.fid_nmx = (uint8_t)(new_fidnum >> 16); + + /* Enter it in the directory (IO$M_CREATE). */ + if (args.modifiers & VMS_ACP_M_CREATE) { + status = acp_dir_mutate(vol, sc, sc->dirhdr, did_num, /*insert*/1, + args.name, (unsigned)strlen(args.name), + new_version, new_fid, + new_fidnum <= ODS2_RESFILES, NULL); + if (status != SS__NORMAL) { + /* Roll back: free the header + its blocks. */ + (void)acp_fid_free(vol, new_fidnum, sc->ibblk); + if (n_ext > 0) + (void)acp_free_file_blocks(vol, sc->filehdr, sc, 1); + args.status = status; /* SS$_DUPLNAM / SS$_DEVICEFULL */ + goto free_sc; + } + } + + /* Optionally access it (IO$M_ACCESS): build a window on the channel. */ + if (args.modifiers & VMS_ACP_M_ACCESS) { + int want_write = (args.acctl & VMS_ACP_ACCTL_WRITE) != 0; + /* Re-read the header we just wrote so the window reflects the + * possibly-EOF-patched allocation. */ + if (acp_read_header(vol, new_fidnum, sc->filehdr, &sc->fh) == SS__NORMAL) + (void)acp_access_from_header(proc, args.chan, sc->filehdr, &sc->fh, + new_fid, new_version, want_write); + } + + args.fid_num = new_fid.fid_num; + args.fid_seq = new_fid.fid_seq; + args.fid_rvn = new_fid.fid_rvn; + args.fid_nmx = new_fid.fid_nmx; + args.out_version = new_version; + args.status = SS__NORMAL; + goto free_sc; + } + + /* ---- IO$_DELETE / IO$_MODIFY: resolve the target file first ---- */ + { + ods2_fid_t file_fid; + uint32_t file_fidnum, did_num = 0; + uint16_t rver = args.version; + int have_dir = 0; + + if (args.fidmode) { + memset(&file_fid, 0, sizeof(file_fid)); + file_fid.fid_num = args.fid_num; + file_fid.fid_seq = args.fid_seq; + file_fid.fid_rvn = args.fid_rvn; + file_fid.fid_nmx = args.fid_nmx; + file_fidnum = ods2_fid_number(&file_fid); + } else { + ods2_fid_t did; + memset(&did, 0, sizeof(did)); + did.fid_num = args.did_num; + did.fid_nmx = args.did_nmx; + did_num = ods2_fid_number(&did); + if (did_num == 0) + did_num = ODS2_FID_MFD; + status = acp_read_header(vol, did_num, sc->dirhdr, &sc->dfh); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + if (!(sc->dfh.fh2_filechar & ODS2_FH2_M_DIRECTORY)) { + args.status = SS__NOSUCHFILE; + goto free_sc; + } + have_dir = 1; + status = acp_dir_find(vol, sc->dirhdr, sc->rw.blk, args.name, + args.version, &file_fid, &rver); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + file_fidnum = ods2_fid_number(&file_fid); + } + + status = acp_read_header(vol, file_fidnum, sc->filehdr, &sc->fh); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + /* Write access is required to delete or modify a file. */ + status = acp_check_access(proc, &sc->fh, 1); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + + if (args.func == VMS_ACP_FOP_DELETE) { + /* Remove the directory entry for this {name, version}. */ + if (have_dir) { + int removed = 0; + status = acp_dir_mutate(vol, sc, sc->dirhdr, did_num, /*remove*/0, + args.name, (unsigned)strlen(args.name), + rver, file_fid, 0, &removed); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + if (!removed) { args.status = SS__NOSUCHFILE; goto free_sc; } + } + /* Deallocate the file (IO$M_DELETE, or a by-FID delete). */ + if ((args.modifiers & VMS_ACP_M_DELETE) || args.fidmode) { + status = acp_free_file_blocks(vol, sc->filehdr, sc, 1); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + status = acp_fid_free(vol, file_fidnum, sc->ibblk); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + /* Invalidate the header so a later ACCESS is SS$_NOSUCHFILE. */ + memset(sc->filehdr, 0, ACP_BLOCK_SIZE); + if (exec_blockdev_write_block(vol->backing_major, vol->backing_minor, + vol->idx_lbn + (file_fidnum - 1u), + sc->filehdr, ACP_BLOCK_SIZE) != 0) { + args.status = SS__DEVNOTMOUNT; + goto free_sc; + } + } + args.fid_num = file_fid.fid_num; + args.fid_nmx = file_fid.fid_nmx; + args.out_version = rver; + args.status = SS__NORMAL; + goto free_sc; + } + + /* ---- IO$_MODIFY: extend / truncate / write attributes ---- */ + { + uint32_t old_hiblk = ods2_recattr_hiblk(&sc->fh.fh2_recattr); + uint32_t old_efblk = ods2_recattr_efblk(&sc->fh.fh2_recattr); + uint32_t new_hiblk = old_hiblk, new_efblk = old_efblk; + uint16_t new_ffbyte = sc->fh.fh2_recattr.fat_ffbyte; + int touched = 0; + + /* EXTEND (FIB$L_EXSZ): allocate + append a retrieval pointer. */ + if (args.exsz > 0) { + uint32_t run_lbn = 0; + acp_snap_from_vol(&sc->snap, vol); + status = acp_bitmap_alloc(&sc->snap, &sc->rw, args.exsz, &run_lbn); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + if (ods2_fh2_map_append(sc->filehdr, run_lbn, args.exsz) != ODS2_OK) { + args.status = SS__DEVICEFULL; /* FH2 map area full */ + goto free_sc; + } + new_hiblk = old_hiblk + args.exsz; + touched = 1; + } + + /* TRUNCATE (to trunc_efblk): free the blocks past it, shrink. */ + if (args.trunc_efblk > 0 && args.trunc_efblk <= new_hiblk) { + /* Free allocation strictly past the new EOF block. */ + if (args.trunc_efblk < new_hiblk) { + status = acp_free_file_blocks(vol, sc->filehdr, sc, + args.trunc_efblk + 1u); + if (status != SS__NORMAL) { args.status = status; goto free_sc; } + /* Rewrite the map to cover only [1, trunc_efblk]. */ + { + struct acp_win_ext fex[ACP_WINDOW_MAX]; + struct acp_winbuild wb; + unsigned mpoff = sc->filehdr[offsetof(ods2_fh2_t, fh2_mpoffset)]; + uint8_t *mp = sc->filehdr + (size_t)mpoff * 2u; + size_t mapcap = ACP_BLOCK_SIZE - (size_t)mpoff * 2u - 2u; + uint32_t want = args.trunc_efblk, e; + + wb.win = fex; wb.max = ACP_WINDOW_MAX; wb.n = 0; + wb.next_vbn = 1; wb.overflow = 0; + if (ods2_fh2_map_walk(sc->filehdr, acp_winbuild_cb, &wb, NULL) + != ODS2_OK) { + args.status = SS__DEVNOTMOUNT; goto free_sc; + } + memset(mp, 0, mapcap); + sc->filehdr[offsetof(ods2_fh2_t, fh2_map_inuse)] = 0; + for (e = 0; e < wb.n && want > 0; e++) { + uint32_t take = fex[e].count < want ? fex[e].count : want; + if (ods2_fh2_map_append(sc->filehdr, fex[e].lbn, take) + != ODS2_OK) { + args.status = SS__DEVICEFULL; goto free_sc; + } + want -= take; + } + } + } + new_hiblk = args.trunc_efblk; + new_efblk = args.trunc_efblk; + new_ffbyte = args.trunc_ffbyte; + touched = 1; + } + + /* WRITE ATTRIBUTES (protection / owner). */ + if (args.attr_ctl & VMS_ACP_ATTR_PROT) { + acp_put16(sc->filehdr + offsetof(ods2_fh2_t, fh2_fileprot), + args.attr.fileprot); + touched = 1; + } + if (args.attr_ctl & VMS_ACP_ATTR_OWNER) { + acp_put16(sc->filehdr + offsetof(ods2_fh2_t, fh2_fileowner) + 0, + args.attr.uic_member); + acp_put16(sc->filehdr + offsetof(ods2_fh2_t, fh2_fileowner) + 2, + args.attr.uic_group); + touched = 1; + } + + if (touched) { + (void)ods2_fh2_set_eof(sc->filehdr, new_hiblk, new_efblk, new_ffbyte); + ods2_fh2_reseal(sc->filehdr); + if (exec_blockdev_write_block(vol->backing_major, vol->backing_minor, + vol->idx_lbn + (file_fidnum - 1u), + sc->filehdr, ACP_BLOCK_SIZE) != 0) { + args.status = SS__DEVNOTMOUNT; + goto free_sc; + } + } + + args.fid_num = file_fid.fid_num; + args.fid_nmx = file_fid.fid_nmx; + args.out_version = rver; + args.new_hiblk = new_hiblk; + args.new_efblk = new_efblk; + args.new_ffbyte = new_ffbyte; + args.status = SS__NORMAL; + goto free_sc; + } + } + +free_sc: + exec_free(sc); + } +#else + args.status = SS__DEVNOTMOUNT; /* no codec: nothing mountable here */ +#endif /* OVMX_ODS2_KERNEL */ + +out: + if (exec_copyout((void *)arg, &args, sizeof(args))) + return -EFAULT; + return 0; +} + /* ================================================================ * $DASSGN fallback and process teardown -- called from vms_devtab.c * ================================================================ */ diff --git a/src/kernel/vms_acp.h b/src/kernel/vms_acp.h index b36f6de77..cb5bc377c 100644 --- a/src/kernel/vms_acp.h +++ b/src/kernel/vms_acp.h @@ -417,4 +417,129 @@ _Static_assert(sizeof(struct vms_acp_acpcontrol_args) == 200, _Static_assert(VMS_IOCTL_ACP_ACPCONTROL == 0xC0C8566Fu, "VMS_IOCTL_ACP_ACPCONTROL encodes differently here than on the reference build"); +/* + * ============================================================================ + * IO$_CREATE / IO$_DELETE / IO$_MODIFY -- the ACP FILE-OPERATION umbrella + * (vms-5303, epic vms-208). The SIXTH and final rung of the ACP-QIO surface: + * create a file header (allocate a real FID from INDEXF.SYS's index bitmap, + * initialize the FH2 from the ATR list, optionally enter it in a directory at + * a new highest version, optionally access it); delete a file (remove its + * directory entry and/or deallocate its header + all its blocks); and modify a + * file (extend/allocate, truncate, write attributes, write a directory entry). + * + * IOCTL-MAPPING DECISION (OVMX design choice, justified). On real OpenVMS these + * are THREE distinct $QIO function codes (IO$_CREATE=9, IO$_DELETE=3, + * IO$_MODIFY=6 in $IODEF), each with the same five FIB/name/ATR parameters. The + * ACP band 0x68-0x6F is FULL (0x68 MOUNT ... 0x6F ACPCONTROL) and 0x70 begins + * the mailbox band, so there is no free ioctl NUMBER for three more per-function + * ioctls. Rather than burn scarce ioctl numbers -- or, worse, restructure the + * already-shipped, ABI-frozen band -- these three route through ONE "ACP + * file-operation" ioctl whose `func` field carries the actual $QIO FUNCTION CODE + * (VMS_ACP_FOP_CREATE/_DELETE/_MODIFY == IO$_CREATE/_DELETE/_MODIFY). This is + * MORE VMS-faithful than one-ioctl-per-function, not less: on VMS $QIO is a + * SINGLE system service and the function code selects the operation, which is + * exactly what carrying the function code in a field reproduces. It also + * extends the umbrella #641 (vms-a0b) deliberately built at nr 0x6F "EXACTLY so + * more ACP ops can route through a func field." + * + * The ioctl REUSES nr 0x6F (the ACP umbrella nr) with its OWN, larger arg + * struct: _IOWR folds sizeof into the request number, so VMS_IOCTL_ACP_FILEOP + * (252-byte struct) and VMS_IOCTL_ACP_ACPCONTROL (200-byte struct) are DISTINCT + * 32-bit ioctl commands -- the kernel switch dispatches each to its own handler, + * and a size drift on either yields -ENOTTY (honest), never a cross-decode, the + * same property every ioctl in this header already relies on. The + * ACPCONTROL struct (search-shaped, ABI-frozen at 200 bytes) cannot itself hold + * the ATR attribute block a CREATE needs, which is why FILEOP is a second + * size-distinct struct on the umbrella nr rather than more `func` subcodes on + * the same struct. + * + * CLEAN-ROOM (CLAUDE.md Rule 8, posture D2). The FIB roles (FIB$W_FID/_DID/ + * _ACCTL, FIB$W_EXCTL/FIB$L_EXSZ extend control, FIB$W_NMCTL FIB$V_NEWVER) and + * the ATR codes are from the public I/O manual + $FIBDEF/$ATRDEF; the byte + * layout of the struct below is an OVMX design choice (labelled), as for the + * other ACP structs. Only the ODS-2 ON-DISK bytes written (the new FH2, the + * INDEXF/BITMAP.SYS bits, the versioned directory record) are byte-authentic, + * via the codec (src/vmsfs/ods2/, ods2_edit.c). + */ + +/* The `func` selector == the $QIO function code ($IODEF): create/delete/modify. */ +#define VMS_ACP_FOP_CREATE 9u /* IO$_CREATE */ +#define VMS_ACP_FOP_DELETE 3u /* IO$_DELETE */ +#define VMS_ACP_FOP_MODIFY 6u /* IO$_MODIFY */ + +/* `modifiers` bits (IO$M_* roles; OVMX-original bit values, Rule 8 D2). */ +#define VMS_ACP_M_CREATE 0x0001u /* IO$M_CREATE: enter the file in a directory */ +#define VMS_ACP_M_ACCESS 0x0002u /* IO$M_ACCESS: also access it (build a window) */ +#define VMS_ACP_M_DELETE 0x0004u /* IO$M_DELETE: also delete the file (dealloc) */ + +/* `attr_ctl` bits: which ATR fields to apply (CREATE/MODIFY write-attributes). */ +#define VMS_ACP_ATTR_PROT 0x01u /* apply attr.fileprot */ +#define VMS_ACP_ATTR_OWNER 0x02u /* apply attr.uic_group/uic_member */ + +/* + * IO$_CREATE / IO$_DELETE / IO$_MODIFY. `func` selects the operation. + * + * - CREATE: allocate a FID (real, from the index bitmap), init the FH2 (kind + * ODS2_FK_*, attrs from `attr` per attr_ctl, owner defaulting to the caller's + * UIC per INV-6), optionally extend by `exsz` initial blocks, optionally + * (VMS_ACP_M_CREATE) enter it in FIB$W_DID at a NEW HIGHEST version (`version` + * 0 => highest+1), optionally (VMS_ACP_M_ACCESS) build a window on `chan`. + * Returns the assigned FID (fid_*) and version (out_version). SS$_DUPLNAM (duplicate name; SS$_DUPFILNAM value not yet lab-pinned) + * if the {name,version} already exists; SS$_DEVICEFULL if INDEXF/BITMAP is + * exhausted; SS$_NOPRIV if the directory write is denied. + * - DELETE: remove the FIB$W_DID directory entry for `name`/`version` (0 => all + * versions) and, if VMS_ACP_M_DELETE (or fidmode by-FID), deallocate the file + * (free every data block in BITMAP.SYS, free the FID in the index bitmap, + * invalidate the header). SS$_NOSUCHFILE if the file/entry does not exist. + * - MODIFY: extend by `exsz` blocks (append a retrieval pointer, grow HIBLK); + * and/or truncate to `trunc_efblk`/`trunc_ffbyte` (free the blocks past it); + * and/or write attributes (fileprot/owner per attr_ctl). Returns the new + * HIBLK/EOF. By FID (fidmode) or by name in FIB$W_DID. + */ +struct vms_acp_fileop_args { + uint32_t chan; /* in: file-class channel */ + uint32_t func; /* in: VMS_ACP_FOP_* ($QIO function code) */ + uint32_t modifiers; /* in: VMS_ACP_M_* (IO$M_CREATE/_ACCESS/_DELETE) */ + uint32_t acctl; /* in: FIB$L_ACCTL (VMS_ACP_M_ACCESS: read/write) */ + uint16_t did_num; /* in: FIB$W_DID directory FID (0/0/0 => MFD) */ + uint16_t did_seq; + uint8_t did_rvn; + uint8_t did_nmx; + uint8_t fidmode; /* in: DELETE/MODIFY by FID (ignore name/DID) */ + uint8_t kind; /* in: CREATE file kind (ODS2_FK_*) */ + uint16_t fid_num; /* in (fidmode) / out (CREATE): FIB$W_FID */ + uint16_t fid_seq; + uint8_t fid_rvn; + uint8_t fid_nmx; + uint16_t version; /* in: wanted/new version (CREATE 0 => highest+1) */ + uint16_t out_version; /* out: resolved/assigned version */ + uint8_t attr_ctl; /* in: VMS_ACP_ATTR_* (which attrs to apply) */ + uint8_t pad0; + uint32_t exsz; /* in: FIB$L_EXSZ blocks to allocate (extend) */ + uint32_t trunc_efblk; /* in: MODIFY truncate-to EOF VBN (0 => no trunc) */ + uint16_t trunc_ffbyte; /* in: MODIFY truncate first-free byte */ + uint16_t pad1; + uint32_t window_nextents; /* out: extents in the built window (M_ACCESS) */ + uint32_t total_blocks; /* out: window total block count */ + uint32_t first_lbn; /* out: LBN of window VBN 1 */ + uint32_t new_hiblk; /* out: highest allocated VBN after the op */ + uint32_t new_efblk; /* out: end-of-file VBN after the op */ + uint32_t new_ffbyte; /* out: first free byte after the op */ + uint32_t pad2; + char name[VMS_ACP_NAME_SIZE]; /* in (P2): "NAME.TYPE" */ + struct vms_acp_fileattr attr; /* in (P5): CREATE/MODIFY attrs / out */ + uint32_t status; /* out: SS$_ */ + uint32_t pad3; +}; + +#define VMS_IOCTL_ACP_FILEOP \ + _IOWR(VMS_IOC_MAGIC, 0x6F, struct vms_acp_fileop_args) + +_Static_assert(sizeof(struct vms_acp_fileop_args) == 252, + "vms_acp_fileop_args changed size -- VMS_IOCTL_ACP_FILEOP ABI break"); +_Static_assert(VMS_IOCTL_ACP_FILEOP == 0xC0FC566Fu, + "VMS_IOCTL_ACP_FILEOP encodes differently here than on the reference build"); +_Static_assert(VMS_IOCTL_ACP_FILEOP != VMS_IOCTL_ACP_ACPCONTROL, + "FILEOP and ACPCONTROL must stay DISTINCT 32-bit commands on the shared nr 0x6F"); + #endif /* _VMS_ACP_H */ diff --git a/src/kernel/vms_internal.h b/src/kernel/vms_internal.h index ceef70077..547e7f817 100644 --- a/src/kernel/vms_internal.h +++ b/src/kernel/vms_internal.h @@ -1081,6 +1081,13 @@ long vms_ioctl_acp_acpcontrol(struct vms_proc *proc, unsigned long arg); */ long vms_ioctl_acp_readvb(struct vms_proc *proc, unsigned long arg); long vms_ioctl_acp_writevb(struct vms_proc *proc, unsigned long arg); +/* + * IO$_CREATE / IO$_DELETE / IO$_MODIFY (vms-5303): the ACP file-operation + * umbrella -- allocate a file header from INDEXF.SYS, enter a versioned + * directory record, deallocate a header + its blocks, extend/truncate/write + * attributes. func-dispatched on VMS_ACP_FOP_*; see vms_acp.h. + */ +long vms_ioctl_acp_fileop(struct vms_proc *proc, unsigned long arg); /* * Release one file-class channel by number, for vms_ioctl_dassgn()'s fallback * when `chan` is neither a device nor a mailbox channel. Returns 0 if `chan` diff --git a/src/kernel/vms_module.c b/src/kernel/vms_module.c index 9e9127021..b77df1c69 100644 --- a/src/kernel/vms_module.c +++ b/src/kernel/vms_module.c @@ -858,6 +858,8 @@ static long vms_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg return vms_ioctl_acp_writevb(proc, arg); case VMS_IOCTL_ACP_ACPCONTROL: return vms_ioctl_acp_acpcontrol(proc, arg); + case VMS_IOCTL_ACP_FILEOP: + return vms_ioctl_acp_fileop(proc, arg); case VMS_IOCTL_MBX_SET_WRTATTN: return vms_ioctl_mbx_set_wrtattn(proc, arg); diff --git a/src/libvmssys/vms_kif.c b/src/libvmssys/vms_kif.c index e978e9358..20823c257 100644 --- a/src/libvmssys/vms_kif.c +++ b/src/libvmssys/vms_kif.c @@ -2219,3 +2219,18 @@ uint32_t vms_kif_acp_acpcontrol(struct vms_acp_acpcontrol_args *args) return args->status; } + +uint32_t vms_kif_acp_fileop(struct vms_acp_fileop_args *args) +{ + if (!args) + return SS$_BADPARAM; + if (!acp_bind_ok()) + return SS$_NOSUCHDEV; + + args->name[VMS_ACP_NAME_SIZE - 1] = '\0'; + args->status = 0; + + KIF_CALL(VMS_IOCTL_ACP_FILEOP, args); + + return args->status; +} diff --git a/src/libvmssys/vms_kif.h b/src/libvmssys/vms_kif.h index b1546af76..59f683fe0 100644 --- a/src/libvmssys/vms_kif.h +++ b/src/libvmssys/vms_kif.h @@ -822,4 +822,16 @@ uint32_t vms_kif_acp_writevb(struct vms_acp_rw_args *args); * rung); exercised by tests/qemu/test_syssvc_acp_search.c against real /dev/vms. */ uint32_t vms_kif_acp_acpcontrol(struct vms_acp_acpcontrol_args *args); +/* + * IO$_CREATE / IO$_DELETE / IO$_MODIFY -- the ACP file-operation umbrella + * (vms-5303): create a file (real FID from INDEXF.SYS, versioned directory + * entry, optional access), delete a file (remove the entry + deallocate header + * and blocks), modify a file (extend/truncate/write attributes). func-dispatched + * on args->func (VMS_ACP_FOP_*). See src/kernel/vms_acp.h for the FIB/ATR + * interface and the ioctl-mapping decision. Returns SS$_NOSUCHDEV if /dev/vms + * is absent. OVMX-UNWIRED: vms_kif_acp_fileop (vms-5303) -- no product caller + * yet (RMS $CREATE/$ERASE/$EXTEND wire to it in a later rung); exercised by + * tests/qemu/test_syssvc_acp_create.c against real /dev/vms. */ +uint32_t vms_kif_acp_fileop(struct vms_acp_fileop_args *args); + #endif /* _VMS_KIF_H */ diff --git a/src/vmsfs/include/vmsfs/ods2.h b/src/vmsfs/include/vmsfs/ods2.h index 57d008a1f..e750764ad 100644 --- a/src/vmsfs/include/vmsfs/ods2.h +++ b/src/vmsfs/include/vmsfs/ods2.h @@ -1479,6 +1479,68 @@ int ods2_sbm_block_bit_free(const void *bitmap_block, unsigned bit_in_block); void ods2_sbm_block_alloc(void *bitmap_block, unsigned bit_in_block); void ods2_sbm_block_free(void *bitmap_block, unsigned bit_in_block); +/* ================================================================ + * FILE-HEADER allocation + creation (vms-5303, epic vms-208) -- the IO$_CREATE + * / IO$_DELETE / IO$_MODIFY write-side twins of ods2_writer.c's + * write_fh2_header_ext() / ifile_bitmap / dir_insert. All PURE (caller-supplied + * block buffers, no I/O, no allocation); the ACP sequences the raw block reads + * and writes around them. See ods2_edit.c for the per-field provenance. + * ================================================================ */ + +/* Index-file bitmap (INDEXF.SYS index bitmap, hm2_ibmaplbn..): OPPOSITE bit + * sense from the storage bitmap -- a SET bit == a FID IN USE. bit_in_block is + * (fidnum-1) % 4096; the block is index (fidnum-1) / 4096 of the bitmap. */ +int ods2_ifbm_block_fid_used(const void *bitmap_block, unsigned bit_in_block); +void ods2_ifbm_block_alloc(void *bitmap_block, unsigned bit_in_block); +void ods2_ifbm_block_free(void *bitmap_block, unsigned bit_in_block); + +/* FH2 file "kind" -- the RECATTR (FAT) / efblk preset, matching + * ods2_writer.c's internal enum fh2_kind values byte-for-byte. */ +enum ods2_fh2_kind { + ODS2_FK_SYSTEM = 0, /* reserved-file stub (rtype 1) */ + ODS2_FK_DIR = 1, /* directory (rtype 2, rattrib 0x08) */ + ODS2_FK_DATA = 2, /* RFM=VAR data file (rtype 2, rattrib CR) */ + ODS2_FK_DATA_FIX = 3 /* RFM=FIXED 512-byte data file (rtype 1) */ +}; + +/* Build a complete FH2 file header into a caller-supplied 512-byte block. + * `owner`={0,0} + `fileprot`=0 => the writer's kind default (SYSTEM [1,4], + * 0xFA00/0xBA00). `extents`/`n_extents` are the file's initial allocation + * (may be 0). Reseals its own checksum. ODS2_ERR_ARGS on a bad fidnum/name; + * ODS2_ERR_NOSPACE if the extents overflow the FH2 map area. */ +ods2_status_t ods2_fh2_build(void *header_block, uint32_t fidnum, uint16_t seq, + const char *name, uint16_t version, uint32_t filechar, + unsigned kind, const ods2_extent_t *extents, + unsigned n_extents, size_t data_len, + ods2_fid_t backlink, ods2_uic_t owner, + uint16_t fileprot, uint32_t maxfiles); + +/* Insert a {name, version, entry_fid} directory record into a directory's + * data blocks (in_blocks = in_nblk contiguous 512-byte blocks), producing the + * repacked result in out_blocks (up to out_nblk_cap blocks) and its block + * count in *out_nblk (may exceed in_nblk -- the ACP then allocates the growth + * and rewrites the FH2 map). `flat` is caller scratch, >= in_nblk*512 + 528. + * is_resfile controls dir_verlimit ([F14]). ODS2_ERR_ARGS on a duplicate + * {name, version}; ODS2_ERR_NOSPACE if the repack exceeds out_nblk_cap. */ +ods2_status_t ods2_dir_insert_blocks(const uint8_t *in_blocks, unsigned in_nblk, + const char *name, unsigned namecount, + uint16_t version, ods2_fid_t entry_fid, + int is_resfile, + uint8_t *flat, size_t flat_cap, + uint8_t *out_blocks, unsigned out_nblk_cap, + unsigned *out_nblk); + +/* Remove `version` (0 => every version) of `name` from a directory's data + * blocks, repacking into out_blocks. Never grows or deallocates directory + * blocks: *out_nblk == in_nblk (trailing blocks emptied), so the ACP leaves + * the FH2 map untouched. *removed set iff a matching entry was dropped. */ +ods2_status_t ods2_dir_remove_blocks(const uint8_t *in_blocks, unsigned in_nblk, + const char *name, unsigned namecount, + uint16_t version, + uint8_t *flat, size_t flat_cap, + uint8_t *out_blocks, unsigned out_nblk_cap, + unsigned *out_nblk, int *removed); + /* ================================================================ * BLOCK-DEVICE-BACKED WRITER (implemented in ods2/ods2_writer.c) -- * increment 11, vms-6d3b, R2 of the real-ODS-2-runtime epic vms-5eb. diff --git a/src/vmsfs/ods2/ods2_edit.c b/src/vmsfs/ods2/ods2_edit.c index b6a79f31b..8812062b3 100644 --- a/src/vmsfs/ods2/ods2_edit.c +++ b/src/vmsfs/ods2/ods2_edit.c @@ -37,6 +37,7 @@ */ #include "vmsfs/ods2.h" +#include "ods2_kcompat.h" /* memset/memcpy/memmove/memcmp/strlen/snprintf (dual-world) */ /* ---- little-endian scalar access (endian-independent), as ods2_writer.c ---- */ @@ -46,6 +47,11 @@ static inline void ed_put16(uint8_t *p, uint16_t v) p[1] = (uint8_t)((v >> 8) & 0xFF); } +static inline uint16_t ed_rd16(const uint8_t *p) +{ + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + static inline void ed_put32(uint8_t *p, uint32_t v) { p[0] = (uint8_t)(v & 0xFF); @@ -199,3 +205,519 @@ void ods2_sbm_block_free(void *bitmap_block, unsigned bit_in_block) w |= (1u << bit_idx); ed_put32(wp, w); } + +/* ================================================================ + * INDEX-FILE bitmap (INDEXF.SYS's index bitmap, at hm2_ibmaplbn..) bit + * accounting -- the FILE-HEADER allocation counterpart of the storage bitmap + * above (vms-5303, epic vms-208). SAME 32-bit-little-endian, 4096-bit-per-block + * packing [N2], but the OPPOSITE bit SENSE: a SET bit == a FID IN USE, a CLEAR + * bit == a FREE FID (this is the sense ods2_writer.c's ifile_bitmap_mark_used() + * writes: `ods2_bitmap_set(..., fidnum - 1, value=1)` to mark FID in use, and + * format_common()'s ods2_bitmap_set(..., i - 1, 1) marks reserved FIDs IN USE). Bit N + * of the whole index bitmap corresponds to file NUMBER N+1 (FID numbers are + * 1-based). These helpers act on ONE 512-byte index-bitmap DATA block; the ACP + * (src/kernel-core/vmsfs_acp.c) chooses which block ((fidnum-1)/4096) and which + * bit within it ((fidnum-1)%4096), reading/writing the block through + * exec_blockdev_*. Write-side twins of the reader's implicit "bit set == used" + * read, no new format fact. + * ================================================================ */ + +/* 1 if file number `bit_in_block+base` is IN USE (bit set) in this bitmap block. */ +int ods2_ifbm_block_fid_used(const void *bitmap_block, unsigned bit_in_block) +{ + const uint8_t *b = (const uint8_t *)bitmap_block; + unsigned word_idx = (bit_in_block % ODS2_SBM_BITS_PER_BLOCK) / ODS2_SBM_BITS_PER_WORD; + unsigned bit_idx = bit_in_block % ODS2_SBM_BITS_PER_WORD; + uint32_t w = ed_rd32(b + (size_t)word_idx * 4); + return (w >> bit_idx) & 1u ? 1 : 0; +} + +/* Mark the FID IN USE (set -> 1) in this index-bitmap block. */ +void ods2_ifbm_block_alloc(void *bitmap_block, unsigned bit_in_block) +{ + uint8_t *b = (uint8_t *)bitmap_block; + unsigned word_idx = (bit_in_block % ODS2_SBM_BITS_PER_BLOCK) / ODS2_SBM_BITS_PER_WORD; + unsigned bit_idx = bit_in_block % ODS2_SBM_BITS_PER_WORD; + uint8_t *wp = b + (size_t)word_idx * 4; + uint32_t w = ed_rd32(wp); + w |= (1u << bit_idx); + ed_put32(wp, w); +} + +/* Mark the FID FREE (clear -> 0) in this index-bitmap block -- IO$_DELETE's + * header deallocation, the inverse of _alloc. */ +void ods2_ifbm_block_free(void *bitmap_block, unsigned bit_in_block) +{ + uint8_t *b = (uint8_t *)bitmap_block; + unsigned word_idx = (bit_in_block % ODS2_SBM_BITS_PER_BLOCK) / ODS2_SBM_BITS_PER_WORD; + unsigned bit_idx = bit_in_block % ODS2_SBM_BITS_PER_WORD; + uint8_t *wp = b + (size_t)word_idx * 4; + uint32_t w = ed_rd32(wp); + w &= ~(1u << bit_idx); + ed_put32(wp, w); +} + +/* ================================================================ + * FH2 file-header CONSTRUCTION (vms-5303). PURE write-side twin of + * ods2_writer.c's write_fh2_header_ext() -- the SAME validated on-disk FH2 + * format (Rule 8), transcribed to operate on a caller-supplied 512-byte block + * with no wvolume, no wblk(), no I/O, no allocation, so the executive ACP + * sequences the raw exec_blockdev_write_block around it exactly as it does for + * the map/EOF edits above. Every field, offset, preset and the acoffset==255 + * / reserved1 / fileowner / highwater provenance is write_fh2_header_ext()'s; + * see that function (and ods2.h's WRITER provenance [F2]/[F4]/[F11]/[F15]/[F16]) + * for the per-field real-VAX-MOUNT bisection trail. The only shape difference + * is that owner UIC and protection are PARAMETERS here (the ACP supplies the + * creating process's UIC per INV-6, or the ATR-list value) rather than the + * writer's uniform SYSTEM [1,4]; pass owner={0,0}+fileprot=0 to take the + * kind default (SYSTEM owner, 0xFA00/0xBA00 prot) the writer uses. + * ================================================================ */ + +#define ED_ID_OFF_WORDS 54 /* ident area at byte 108 (write_fh2_header_ext) */ +#define ED_MP_OFF_WORDS 114 /* map area at byte 228 */ + +ods2_status_t ods2_fh2_build(void *header_block, uint32_t fidnum, uint16_t seq, + const char *name, uint16_t version, uint32_t filechar, + unsigned kind, const ods2_extent_t *extents, + unsigned n_extents, size_t data_len, + ods2_fid_t backlink, ods2_uic_t owner, + uint16_t fileprot, uint32_t maxfiles) +{ + uint8_t *h = (uint8_t *)header_block; + char idbuf[20 + 66 + 1]; + size_t base_len, n; + ods2_status_t st; + uint8_t rtype, rattrib; + uint16_t rsize, maxrec, ffbyte; + uint32_t hiblk, efblk, total_count; + unsigned ei; + ods2_uic_t eff_owner = owner; + uint16_t eff_prot = fileprot; + + if (!h || !name) + return ODS2_ERR_ARGS; + if (fidnum < 1 || (maxfiles && fidnum > maxfiles)) + return ODS2_ERR_ARGS; + if (n_extents > 0 && !extents) + return ODS2_ERR_ARGS; + + total_count = 0; + for (ei = 0; ei < n_extents; ei++) + total_count += extents[ei].count; + + base_len = strlen(name); + if (base_len == 0) + return ODS2_ERR_ARGS; + n = (size_t)snprintf(idbuf, sizeof(idbuf), "%s;%u", name, (unsigned)version); + if (n >= sizeof(idbuf)) + return ODS2_ERR_ARGS; + + memset(h, 0, ODS2_BLOCK_SIZE); + + h[offsetof(ods2_fh2_t, fh2_idoffset)] = ED_ID_OFF_WORDS; + h[offsetof(ods2_fh2_t, fh2_mpoffset)] = ED_MP_OFF_WORDS; + h[offsetof(ods2_fh2_t, fh2_acoffset)] = 255; /* [F4] no-ACL sentinel */ + h[offsetof(ods2_fh2_t, fh2_rsoffset)] = 255; + + ed_put16(h + offsetof(ods2_fh2_t, fh2_struclev), ODS2_STRUCLEV_V2); + + ed_put16(h + offsetof(ods2_fh2_t, fh2_fid) + 0, (uint16_t)(fidnum & 0xFFFF)); + ed_put16(h + offsetof(ods2_fh2_t, fh2_fid) + 2, seq); + h[offsetof(ods2_fh2_t, fh2_fid) + 4] = 0; /* rvn */ + h[offsetof(ods2_fh2_t, fh2_fid) + 5] = (uint8_t)(fidnum >> 16);/* nmx */ + + ed_put16(h + offsetof(ods2_fh2_t, fh2_backlink) + 0, backlink.fid_num); + ed_put16(h + offsetof(ods2_fh2_t, fh2_backlink) + 2, backlink.fid_seq); + h[offsetof(ods2_fh2_t, fh2_backlink) + 4] = backlink.fid_rvn; + h[offsetof(ods2_fh2_t, fh2_backlink) + 5] = backlink.fid_nmx; + + ed_put32(h + offsetof(ods2_fh2_t, fh2_filechar), filechar); + + /* ---- RECATTR (FAT) preset by kind (write_fh2_header_ext's switch) ---- */ + switch (kind) { + case ODS2_FK_DIR: + rtype = 2; rattrib = 0x08; rsize = 512; maxrec = 512; + break; + case ODS2_FK_DATA: + rtype = ODS2_RTYPE_VAR; rattrib = ODS2_RAT_CR; rsize = 0; maxrec = 0; + break; + case ODS2_FK_DATA_FIX: + rtype = ODS2_RTYPE_FIX; rattrib = 0x00; rsize = 512; maxrec = 512; + break; + default: /* ODS2_FK_SYSTEM */ + rtype = 1; rattrib = 0x00; rsize = 512; maxrec = 512; + break; + } + if (total_count == 0) { + hiblk = 0; efblk = 1; ffbyte = 0; + } else { + hiblk = total_count; + if (kind == ODS2_FK_DIR) { + efblk = total_count + 1; + ffbyte = 0; + } else { + efblk = total_count; + if ((kind == ODS2_FK_DATA || kind == ODS2_FK_DATA_FIX) && data_len > 0) { + size_t last = data_len - (size_t)(total_count - 1) * ODS2_BLOCK_SIZE; + ffbyte = (uint16_t)last; + } else { + ffbyte = 0; + } + } + } + h[offsetof(ods2_fh2_t, fh2_recattr) + offsetof(ods2_recattr_t, fat_rtype)] = rtype; + h[offsetof(ods2_fh2_t, fh2_recattr) + offsetof(ods2_recattr_t, fat_rattrib)] = rattrib; + ed_put16(h + offsetof(ods2_fh2_t, fh2_recattr) + offsetof(ods2_recattr_t, fat_rsize), rsize); + ed_put16(h + offsetof(ods2_fh2_t, fh2_recattr) + offsetof(ods2_recattr_t, fat_hiblk) + 0, + (uint16_t)(hiblk >> 16)); + ed_put16(h + offsetof(ods2_fh2_t, fh2_recattr) + offsetof(ods2_recattr_t, fat_hiblk) + 2, + (uint16_t)(hiblk & 0xFFFF)); + ed_put16(h + offsetof(ods2_fh2_t, fh2_recattr) + offsetof(ods2_recattr_t, fat_efblk) + 0, + (uint16_t)(efblk >> 16)); + ed_put16(h + offsetof(ods2_fh2_t, fh2_recattr) + offsetof(ods2_recattr_t, fat_efblk) + 2, + (uint16_t)(efblk & 0xFFFF)); + ed_put16(h + offsetof(ods2_fh2_t, fh2_recattr) + offsetof(ods2_recattr_t, fat_ffbyte), ffbyte); + ed_put16(h + offsetof(ods2_fh2_t, fh2_recattr) + offsetof(ods2_recattr_t, fat_maxrec), maxrec); + + /* [F11] owner / prot / reserved1 / highwater. Owner+prot from the caller + * (INV-6: the creating process's UIC), falling to the writer's SYSTEM/kind + * default when the caller passes zero. */ + if (eff_owner.uic_group == 0 && eff_owner.uic_member == 0) { + eff_owner.uic_member = 4; eff_owner.uic_group = 1; /* SYSTEM [1,4] */ + } + if (eff_prot == 0) + eff_prot = (kind == ODS2_FK_DIR) ? 0xBA00u : 0xFA00u; + ed_put16(h + offsetof(ods2_fh2_t, fh2_reserved1), + (fidnum <= ODS2_RESFILES) ? 0xFE00u : 0u); + ed_put16(h + offsetof(ods2_fh2_t, fh2_fileowner) + 0, eff_owner.uic_member); + ed_put16(h + offsetof(ods2_fh2_t, fh2_fileowner) + 2, eff_owner.uic_group); + ed_put16(h + offsetof(ods2_fh2_t, fh2_fileprot), eff_prot); + ed_put32(h + offsetof(ods2_fh2_t, fh2_highwater), hiblk + 1u); + + /* ident area: "NAME.TYPE;VERSION", space-padded (write_fh2_header_ext). */ + { + uint8_t *id = h + (size_t)ED_ID_OFF_WORDS * 2; + size_t first_len = (n < 20) ? n : 20; + size_t ext_len = (n > 20) ? (n - 20) : 0; + + memset(id, ' ', 20); + memcpy(id, idbuf, first_len); + ed_put16(id + 20, version); /* fi2_revision */ + if (ext_len > 0) { + uint8_t *ext = id + offsetof(ods2_ident_t, fi2_filenamext); + memset(ext, ' ', sizeof(((ods2_ident_t *)0)->fi2_filenamext)); + memcpy(ext, idbuf + 20, ext_len); + } + } + + /* map area: one FM2 format-1 retrieval pointer per extent. */ + for (ei = 0; ei < n_extents; ei++) { + st = ods2_fh2_map_append(h, extents[ei].lbn, extents[ei].count); + if (st != ODS2_OK) + return st; + } + + ed_put16(h + offsetof(ods2_fh2_t, fh2_checksum), ods2_block_checksum(h)); + return ODS2_OK; +} + +/* ================================================================ + * DIRECTORY-RECORD build + rebuild (vms-5303). PURE write-side twins of + * ods2_writer.c's ods2_wvolume_dir_insert() / merge_dir_record(): the SAME + * validated on-disk directory format ([F13] name-sorted, [F14] verlimit, + * [F17] one-record-never-crosses-a-block, value entries descending by + * version), transcribed to operate on caller-supplied CONTIGUOUS block buffers + * (in_blocks = the directory's current data blocks, out_blocks = the repacked + * result) with a caller-supplied `flat` scratch buffer -- no wvolume, no + * wblk(), no I/O, no block allocation. The ACP reads the directory's data + * blocks (via its window), calls these to compute the new block bytes, then + * allocates any growth (acp_bitmap_alloc) and writes the blocks + the header + * map back. Every byte layout is ods2_wvolume_dir_insert()'s; see that + * function's [F13]/[F17]/[vms-9794] provenance. + * ================================================================ */ + +/* [F13] byte-wise ascending name order (ods2_writer.c dir_name_cmp twin). */ +static int ed_dir_name_cmp(const char *a, unsigned alen, const char *b, unsigned blen) +{ + unsigned m = alen < blen ? alen : blen; + int c = m ? memcmp(a, b, m) : 0; + if (c != 0) + return c; + if (alen != blen) + return alen < blen ? -1 : 1; + return 0; +} + +/* Merge a new {version, entry_fid} into an existing SAME-name record's + * descending value-entry array -- ods2_writer.c merge_dir_record() twin. + * `src` points at the record's dir_size word; `src_len` == 2 + dir_size. */ +static ods2_status_t ed_merge_dir_record(const uint8_t *src, unsigned src_len, + unsigned namecount, uint16_t version, + ods2_fid_t entry_fid, + uint8_t *out, unsigned *out_len) +{ + unsigned val_off = 6u + namecount; + unsigned n_old, i, new_idx, new_len; + + if (val_off & 1) + val_off++; + if (val_off > src_len || ((src_len - val_off) % 8u) != 0) + return ODS2_ERR_FORMAT; + n_old = (src_len - val_off) / 8u; + + new_idx = n_old; + for (i = 0; i < n_old; i++) { + uint16_t v = ed_rd16(src + val_off + i * 8u); + if (v == version) + return ODS2_ERR_ARGS; /* duplicate version */ + if (v < version) { new_idx = i; break; } + } + + new_len = val_off + (n_old + 1u) * 8u; + if (new_len > ODS2_BLOCK_SIZE - 2u) + return ODS2_ERR_NOSPACE; + + memset(out, 0xFF, new_len); + ed_put16(out + 0, (uint16_t)(new_len - 2)); + memcpy(out + 2, src + 2, 4); /* verlimit + flags + namecount */ + memcpy(out + 6, src + 6, namecount); + for (i = 0; i < new_idx; i++) + memcpy(out + val_off + i * 8u, src + val_off + i * 8u, 8u); + ed_put16(out + val_off + new_idx * 8u + 0, version); + ed_put16(out + val_off + new_idx * 8u + 2, entry_fid.fid_num); + ed_put16(out + val_off + new_idx * 8u + 4, entry_fid.fid_seq); + out[val_off + new_idx * 8u + 6] = entry_fid.fid_rvn; + out[val_off + new_idx * 8u + 7] = entry_fid.fid_nmx; + for (i = new_idx; i < n_old; i++) + memcpy(out + val_off + (i + 1u) * 8u, src + val_off + i * 8u, 8u); + + *out_len = new_len; + return ODS2_OK; +} + +/* Greedy-pack a flat sorted record stream into 512-byte blocks (each 0xFF- + * filled so trailing space reads back as ODS2_DIR_END), reserving 2 trailing + * bytes per block for the terminator. [F17] rule 2. Returns block count via + * *nblk_out; ODS2_ERR_NOSPACE if it exceeds out_nblk_cap. */ +static ods2_status_t ed_dir_pack(const uint8_t *flat, size_t flat_used, + uint8_t *out_blocks, unsigned out_nblk_cap, + unsigned *nblk_out) +{ + unsigned nblk = 1, cur = 0, bi; + size_t foff; + + for (foff = 0; foff < flat_used; ) { + unsigned reclen = 2u + ed_rd16(flat + foff); + if (cur + reclen + 2u > ODS2_BLOCK_SIZE) { nblk++; cur = 0; } + cur += reclen; + foff += reclen; + } + if (nblk > out_nblk_cap) + return ODS2_ERR_NOSPACE; + + for (bi = 0; bi < nblk; bi++) + memset(out_blocks + (size_t)bi * ODS2_BLOCK_SIZE, 0xFF, ODS2_BLOCK_SIZE); + bi = 0; + cur = 0; + for (foff = 0; foff < flat_used; ) { + unsigned reclen = 2u + ed_rd16(flat + foff); + if (cur + reclen + 2u > ODS2_BLOCK_SIZE) { bi++; cur = 0; } + memcpy(out_blocks + (size_t)bi * ODS2_BLOCK_SIZE + cur, flat + foff, reclen); + cur += reclen; + foff += reclen; + } + *nblk_out = nblk; + return ODS2_OK; +} + +ods2_status_t ods2_dir_insert_blocks(const uint8_t *in_blocks, unsigned in_nblk, + const char *name, unsigned namecount, + uint16_t version, ods2_fid_t entry_fid, + int is_resfile, + uint8_t *flat, size_t flat_cap, + uint8_t *out_blocks, unsigned out_nblk_cap, + unsigned *out_nblk) +{ + uint8_t newrec[ODS2_BLOCK_SIZE]; + unsigned new_valoff, newrec_len, b; + size_t flat_used = 0, insert_off = 0, need; + int have_insert = 0, found_name = 0; + ods2_status_t st; + + if (!in_blocks || !name || !flat || !out_blocks || !out_nblk) + return ODS2_ERR_ARGS; + if (namecount == 0 || namecount > 255) + return ODS2_ERR_ARGS; + + need = (size_t)in_nblk * ODS2_BLOCK_SIZE + ODS2_BLOCK_SIZE + 16; + if (flat_cap < need) + return ODS2_ERR_NOSPACE; + + /* Build the new record (ods2_wvolume_dir_insert lines 2100-2116). */ + new_valoff = 6u + namecount; + if (new_valoff & 1) + new_valoff++; + newrec_len = new_valoff + 8u; /* one value entry */ + memset(newrec, 0xFF, sizeof(newrec)); + ed_put16(newrec + 0, (uint16_t)(newrec_len - 2)); + ed_put16(newrec + 2, is_resfile ? version : ODS2_DIR_VERLIMIT_DEFAULT); + newrec[4] = 0; /* dir_flags */ + newrec[5] = (uint8_t)namecount; + memcpy(newrec + 6, name, namecount); + ed_put16(newrec + new_valoff + 0, version); + ed_put16(newrec + new_valoff + 2, entry_fid.fid_num); + ed_put16(newrec + new_valoff + 4, entry_fid.fid_seq); + newrec[new_valoff + 6] = entry_fid.fid_rvn; + newrec[new_valoff + 7] = entry_fid.fid_nmx; + + /* Flatten existing records; locate the sorted insertion point / merge. */ + for (b = 0; b < in_nblk; b++) { + const uint8_t *blk = in_blocks + (size_t)b * ODS2_BLOCK_SIZE; + unsigned off = 0; + for (;;) { + uint16_t rec_size; + unsigned reclen, nc; + if (off + 6 > ODS2_BLOCK_SIZE) + break; + rec_size = ed_rd16(blk + off); + if (rec_size == ODS2_DIR_END) + break; + reclen = 2u + rec_size; + if (off + reclen > ODS2_BLOCK_SIZE) + return ODS2_ERR_FORMAT; + nc = blk[off + 5]; + if (6u + nc > reclen) + return ODS2_ERR_FORMAT; + if (!found_name && !have_insert) { + int cmp = ed_dir_name_cmp(name, namecount, + (const char *)blk + off + 6, nc); + if (cmp == 0) { + unsigned merged_len; + st = ed_merge_dir_record(blk + off, reclen, nc, version, + entry_fid, flat + flat_used, &merged_len); + if (st != ODS2_OK) + return st; + flat_used += merged_len; + found_name = 1; + off += reclen; + continue; + } + if (cmp < 0) { insert_off = flat_used; have_insert = 1; } + } + memcpy(flat + flat_used, blk + off, reclen); + flat_used += reclen; + off += reclen; + } + } + + if (!found_name) { + if (!have_insert) + insert_off = flat_used; + if (insert_off < flat_used) + memmove(flat + insert_off + newrec_len, flat + insert_off, + flat_used - insert_off); + memcpy(flat + insert_off, newrec, newrec_len); + flat_used += newrec_len; + } + + return ed_dir_pack(flat, flat_used, out_blocks, out_nblk_cap, out_nblk); +} + +ods2_status_t ods2_dir_remove_blocks(const uint8_t *in_blocks, unsigned in_nblk, + const char *name, unsigned namecount, + uint16_t version, + uint8_t *flat, size_t flat_cap, + uint8_t *out_blocks, unsigned out_nblk_cap, + unsigned *out_nblk, int *removed) +{ + unsigned b; + size_t flat_used = 0, need; + + if (!in_blocks || !name || !flat || !out_blocks || !out_nblk || !removed) + return ODS2_ERR_ARGS; + if (namecount == 0 || namecount > 255) + return ODS2_ERR_ARGS; + *removed = 0; + + need = (size_t)in_nblk * ODS2_BLOCK_SIZE + 16; + if (flat_cap < need) + return ODS2_ERR_NOSPACE; + + for (b = 0; b < in_nblk; b++) { + const uint8_t *blk = in_blocks + (size_t)b * ODS2_BLOCK_SIZE; + unsigned off = 0; + for (;;) { + uint16_t rec_size; + unsigned reclen, nc, val_off, n_ent, i, keep; + if (off + 6 > ODS2_BLOCK_SIZE) + break; + rec_size = ed_rd16(blk + off); + if (rec_size == ODS2_DIR_END) + break; + reclen = 2u + rec_size; + if (off + reclen > ODS2_BLOCK_SIZE) + return ODS2_ERR_FORMAT; + nc = blk[off + 5]; + if (6u + nc > reclen) + return ODS2_ERR_FORMAT; + + if (!ed_dir_name_cmp(name, namecount, (const char *)blk + off + 6, nc)) { + /* Matching name: drop the requested version(s). */ + val_off = 6u + nc; + if (val_off & 1) val_off++; + if (val_off > reclen || ((reclen - val_off) % 8u) != 0) + return ODS2_ERR_FORMAT; + n_ent = (reclen - val_off) / 8u; + if (version == 0) { + *removed = 1; /* whole name gone */ + off += reclen; + continue; + } + /* Rebuild keeping every value entry except `version`. */ + keep = 0; + { + uint8_t *dst = flat + flat_used; + unsigned dst_val = val_off, wrote = 0; + memset(dst, 0xFF, val_off); + memcpy(dst + 2, blk + off + 2, 4); /* verlimit/flags/nc */ + memcpy(dst + 6, blk + off + 6, nc); + for (i = 0; i < n_ent; i++) { + const uint8_t *ent = blk + off + val_off + i * 8u; + if (ed_rd16(ent) == version) { *removed = 1; continue; } + memcpy(dst + dst_val + wrote * 8u, ent, 8u); + wrote++; + } + keep = wrote; + if (keep > 0) { + unsigned nl = val_off + keep * 8u; + ed_put16(dst + 0, (uint16_t)(nl - 2)); + flat_used += nl; + } + /* keep==0 -> the whole record is dropped (last version). */ + } + off += reclen; + continue; + } + memcpy(flat + flat_used, blk + off, reclen); + flat_used += reclen; + off += reclen; + } + } + + /* Never grow / never deallocate directory blocks on remove -- repack, then + * PAD OUT to the original block count so the file keeps its allocation + * (trailing blocks become empty ODS2_DIR_END); the ACP therefore never + * rewrites the directory's FH2 map on a delete. */ + { + unsigned packed = 0, bi; + ods2_status_t st = ed_dir_pack(flat, flat_used, out_blocks, + in_nblk > out_nblk_cap ? out_nblk_cap : in_nblk, + &packed); + if (st != ODS2_OK) + return st; + for (bi = packed; bi < in_nblk && bi < out_nblk_cap; bi++) + memset(out_blocks + (size_t)bi * ODS2_BLOCK_SIZE, 0xFF, ODS2_BLOCK_SIZE); + *out_nblk = (in_nblk <= out_nblk_cap) ? in_nblk : out_nblk_cap; + } + return ODS2_OK; +} diff --git a/src/vmslink/libvmssys_shr.vec b/src/vmslink/libvmssys_shr.vec index 1aad8f86f..53b7b0566 100644 --- a/src/vmslink/libvmssys_shr.vec +++ b/src/vmslink/libvmssys_shr.vec @@ -141,3 +141,11 @@ vms_kif_acp_writevb=PROCEDURE # shifts an existing index. OVMX-UNWIRED (test-only) today; exported so the # vector is stable when the RMS $SEARCH / DCL F$SEARCH rung wires it. vms_kif_acp_acpcontrol=PROCEDURE +# --- Files-11 (ODS-2) ACP IO$_CREATE / IO$_DELETE / IO$_MODIFY (vms-5303, epic +# vms-208). The ACP file-operation umbrella: create a file header (real FID from +# INDEXF.SYS + versioned directory entry + optional access), delete a file +# (remove entry + deallocate header/blocks), modify (extend/truncate/write +# attributes). func-dispatched. APPENDED at the end per the append-only contract +# -- a new universal never shifts an existing index. OVMX-UNWIRED (test-only) +# today; exported so the vector is stable when RMS $CREATE/$ERASE/$EXTEND wire it. +vms_kif_acp_fileop=PROCEDURE diff --git a/tests/qemu/facility_defects.sh b/tests/qemu/facility_defects.sh index 2b0d1699c..4f9a7f2ec 100755 --- a/tests/qemu/facility_defects.sh +++ b/tests/qemu/facility_defects.sh @@ -496,6 +496,7 @@ acp-mount-nonods2-accepted acp-access-window-vbn-offbyone acp-writevb-extend-alloc-offbyone acp-search-cursor-skips-versions +acp-create-header-slot-offbyone p0-map-not-recorded p1-map-not-recorded p0-unmap-clears-p1 @@ -4905,6 +4906,39 @@ multi-version iteration -- the highest-only (;0) listing, the exact-version (;N) match, the no-match pattern, the empty-context and bad-channel fail-honest checks, and the FID round-trip -- never reaches the inverted branch and stays green. +EOF + ;; + esac;; + + acp-create-header-slot-offbyone) + case "$_f" in + facility) echo "Files-11 (ODS-2) ACP IO\$_CREATE file-header allocation (VMS_IOCTL_ACP_FILEOP allocates a real FID from INDEXF.SYS's index bitmap and writes the new FH2 at that FID's header slot, idx_lbn + (FID - 1)), vms-5303, epic vms-208";; + targets) echo "kernel-core/vmsfs_acp.c";; + suites_red) echo "test_syssvc_acp_create";; + blind_suites) echo "";; + blind_why) echo "";; + isolation) echo "isolated";; + why) echo "The IO\$_CREATE handler writes the freshly built file header at its INDEXF slot -- vol->idx_lbn + (new_fidnum - 1u) -- the same header-number arithmetic acp_read_header uses to READ a header (header N at idx_lbn + (N-1)). Dropping the -1 writes the header ONE SLOT TOO HIGH (at FID new_fidnum+1's slot). The CREATE still marks new_fidnum's index-bitmap bit used, still returns new_fidnum, and still enters the directory record pointing at new_fidnum -- so the create reports success -- but the FID the directory now resolves to (new_fidnum) has no valid header at its own slot: acp_read_header(new_fidnum) reads the wrong (unwritten/stale) block, fails to parse, and returns SS\$_NOSUCHFILE. The file therefore cannot be re-opened by name. Only an assertion that re-ACCESSes the created file BY NAME (resolving the directory record to the FID and reading the header at its slot) can tell; the CREATE call's own status, the assigned FID, and the fail-honest edge checks (bad func, non-directory DID, delete-nonexistent) never re-read the header at its slot and stay green.";; + require_fail) cat <<'EOF' +IO$_ACCESS CREAT.TST by name resolves the created FID at version 1 (header at its INDEXF slot) +EOF + ;; + knock_on_fail) cat <<'EOF' +after DEACCESS + re-ACCESS the written bytes persist -- it hit the platter (INV-6) +IO$_ACCESS CREAT.TST;1 still resolves the FIRST file (both versions coexist) +EOF + ;; + knock_on_why) cat <<'EOF' +THE SAME ONE MISPLACED HEADER, SEEN AT EVERY LATER BY-NAME ACCESS OF THE FILE. +Once CREAT.TST;1's header is written one slot too high, EVERY operation that +resolves the name to its FID and reads the header at that FID's slot fails the +same way: the write-persistence proof re-ACCESSes CREAT.TST to read its bytes +back (require_fail already reddened the first such access, this is the same +translation at a later step), and the ;1-still-resolves proof (after ;2 is +created) re-opens CREAT.TST;1 by its explicit version -- both read the header at +new_fidnum's slot, find no valid header, and observe SS$_NOSUCHFILE instead of +the file. The write itself, the ;2 create (a DIFFERENT FID, its own slot), and +the fail-honest edges never read fid1's header at its slot and stay green. EOF ;; esac;; @@ -6318,6 +6352,18 @@ apply_edit() { # nothing (the no-op selftest requires). sed -i 's|return version < c->prev_ver;|return version > c->prev_ver; /* NEGCTL acp-search-cursor-skips-versions */|' "$_file";; + acp-create-header-slot-offbyone) + # ANCHORED to the single IO$_CREATE header-slot write in + # vms_ioctl_acp_fileop: `vol->idx_lbn + (new_fidnum - 1u)` occurs exactly + # once in the file (the variable `new_fidnum` is unique to the CREATE + # path; acp_read_header/DELETE/MODIFY use `fid_num`/`file_fidnum`). + # Dropping the `- 1u` writes the new header one slot too high, so the + # created file cannot be re-opened by name -- only the negctl-anchored + # re-ACCESS-by-name assertions redden. After substitution the original + # text is gone, so a second apply matches nothing (the no-op selftest + # requires). + sed -i 's|vol->idx_lbn + (new_fidnum - 1u)|vol->idx_lbn + (new_fidnum) /* NEGCTL acp-create-header-slot-offbyone */|' "$_file";; + p0-map-not-recorded) # RANGE-ANCHORED to vms_ioctl_p0_map's own body: `proc->p0_base = # args.base;` immediately followed by `proc->p0_limit = args.limit;` diff --git a/tests/qemu/test_syssvc_acp_create.c b/tests/qemu/test_syssvc_acp_create.c new file mode 100644 index 000000000..91911c66c --- /dev/null +++ b/tests/qemu/test_syssvc_acp_create.c @@ -0,0 +1,332 @@ +/* + * test_syssvc_acp_create.c - the Files-11 (ODS-2) ACP answers IO$_CREATE / + * IO$_DELETE / IO$_MODIFY: allocate a real file header from INDEXF.SYS, enter a + * versioned directory record, deallocate a header + its blocks, and + * extend/truncate/write attributes -- all against a real /dev/vms (vms-5303, + * epic vms-208). + * + * The SIXTH and final rung of the executive ACP-QIO surface (after the + * kernel-resident codec vms-dcd, the channel front-end vms-149, the + * executive-global $MOUNT vms-127, IO$_ACCESS/DEACCESS vms-204, + * IO$_READVBLK/WRITEVBLK vms-c60, and IO$_ACPCONTROL/$SEARCH vms-a0b). On real + * OpenVMS these are $QIO function codes (IO$_CREATE/IO$_DELETE/IO$_MODIFY) on a + * channel $ASSIGNed to a mounted volume, serviced by the XQP in the caller's + * context; RMS $CREATE/$ERASE/$EXTEND are layered on them (VSI I/O User's + * Reference, "ACP-QIO Interface"). OVMX reaches the executive over /dev/vms via + * vms_kif_acp_fileop(), whose `func` field carries the $QIO function code. + * + * WHAT THIS SUITE PROVES, through the sys$/kif API against a real /dev/vms, over + * the real-VAX ODS-2 fixture the harness seeds WRITABLE on DKA0:: + * + * 1. IO$_CREATE ALLOCATES A REAL FILE. Creating [OVMXDIR]CREAT.TST assigns a + * genuine FID from INDEXF.SYS's index bitmap (a new file number, distinct + * from every reserved and existing file), enters it in the directory at + * version ;1, and the file is READABLE BACK BY NAME (the directory record + * resolves to the assigned FID; the header parses at its INDEXF slot). + * 2. THE FILE IS A REAL, WRITABLE, DURABLE FILE. WRITEVBLK to the freshly + * created (initially empty) file extends + writes; after DEACCESS + + * re-ACCESS the bytes persist -- it hit the platter (INV-6). + * 3. VERSIONS. Creating CREAT.TST again yields ;2 (a SECOND file, a SECOND + * FID); the directory record now carries both versions, highest first. + * 4. IO$_DELETE REMOVES + DEALLOCATES. Deleting ;2 removes its directory + * entry, frees its blocks in BITMAP.SYS and its FID in the index bitmap; + * ACCESS of ;2 is then SS$_NOSUCHFILE, while ;1 still resolves. Deleting + * ;1 empties the name entirely: ACCESS is SS$_NOSUCHFILE. + * 5. IO$_MODIFY EXTENDS / TRUNCATES / WRITES ATTRIBUTES, and each persists + * (re-read exact): extend grows HIBLK, truncate shrinks EOF + frees blocks, + * a protection change survives DEACCESS + re-ACCESS. + * 6. FAIL-HONEST EDGES (INV-6, never a fabricated success): an unknown $QIO + * function is SS$_BADPARAM; CREATE into a DID that is not a directory is + * SS$_NOSUCHFILE; DELETE of a name that does not exist is SS$_NOSUCHFILE. + * + * NOT TESTABLE IN THIS HARNESS (stated, not faked -- CLAUDE.md Rule 10): + * SS$_NOPRIV (a protection-denied create) needs an UNPRIVILEGED caller, and + * SS$_DEVICEFULL needs an exhausted INDEXF/BITMAP; the QEMU harness runs as + * the all-privileged SYSTEM identity against a 16 MB fixture, so neither is + * reachable here. The protection GATE itself is exercised by the (already + * green) acp_access/acp_rw suites' NOPRIV assertions. + * + * ORDERING / ISOLATION. This suite MUTATES its DKA0: fixture COPY: it creates + * files in [OVMXDIR] under UNIQUE names (CREAT.TST / MODF.TST -- never HELLO.TXT, + * which the acp_access / acp_rw suites read) and DELETES every file it creates, + * so the net directory state is restored (only BITMAP/INDEXF bits cycle, + * harmlessly). Each shard boots its OWN fresh writable fixture copy + * (run_tests.sh cp per boot); this suite sorts alphabetically BEFORE acp_mount / + * acp_rw, and touches no file they read. Same reasoning as test_syssvc_acp_rw.c. + * + * NO /dev/vms -> honest SKIP (77), never a fake pass: IO$_CREATE/DELETE/MODIFY + * are executive-resident, so with no /dev/vms there is nothing to assert. + */ + +#include +#include +#include +#include + +#include "starlet.h" +#include "descrip.h" +#include "ssdef.h" +#include "vms_kif.h" +#include "vms/pcb.h" +#include "vmsfs/ods2.h" /* ODS2_FK_* file-kind selectors for CREATE */ + +#define EXIT_SKIP 77 +#define ODS2_UNIT "DKA0:" +#define OVMXDIR_FID_NUM 11u /* [OVMXDIR] in the real-VAX fixture */ + +static int pass = 0; +static int fail = 0; + +static void check(int cond, const char *name) +{ + if (cond) { printf(" PASS: %s\n", name); pass++; } + else { printf(" FAIL: %s\n", name); fail++; } +} + +static int executive_present(void) +{ + int fd = vms_kif_open(); + if (fd < 0) + return 0; + vms_kif_close(); + return 1; +} + +/* IO$_CREATE [OVMXDIR];highest+1>, optionally accessed. */ +static uint32_t create_file(uint32_t chan, const char *name, uint16_t version, + unsigned modifiers, int want_write, + struct vms_acp_fileop_args *f) +{ + memset(f, 0, sizeof(*f)); + f->chan = chan; + f->func = VMS_ACP_FOP_CREATE; + f->modifiers = modifiers; + f->kind = ODS2_FK_DATA_FIX; + f->did_num = OVMXDIR_FID_NUM; + f->did_seq = 1; + f->version = version; + if (want_write) + f->acctl = VMS_ACP_ACCTL_WRITE; + strncpy(f->name, name, VMS_ACP_NAME_SIZE - 1); + return vms_kif_acp_fileop(f); +} + +/* IO$_DELETE [OVMXDIR]; (M_DELETE => also deallocate). */ +static uint32_t delete_file(uint32_t chan, const char *name, uint16_t version, + struct vms_acp_fileop_args *f) +{ + memset(f, 0, sizeof(*f)); + f->chan = chan; + f->func = VMS_ACP_FOP_DELETE; + f->modifiers = VMS_ACP_M_DELETE; + f->did_num = OVMXDIR_FID_NUM; + f->did_seq = 1; + f->version = version; + strncpy(f->name, name, VMS_ACP_NAME_SIZE - 1); + return vms_kif_acp_fileop(f); +} + +/* IO$_ACCESS [OVMXDIR];highest> for read/write. */ +static uint32_t access_named(uint32_t chan, const char *name, uint16_t version, + int want_write, struct vms_acp_access_args *a) +{ + memset(a, 0, sizeof(*a)); + a->chan = chan; + a->did_num = OVMXDIR_FID_NUM; + a->did_seq = 1; + a->version = version; + if (want_write) + a->acctl = VMS_ACP_ACCTL_WRITE; + strncpy(a->name, name, VMS_ACP_NAME_SIZE - 1); + return vms_kif_acp_access(a); +} + +int main(void) +{ + struct vms_acp_fileop_args f; + struct vms_acp_access_args a; + struct vms_acp_rw_args r; + uint32_t st, chan = 0; + uint32_t fid1 = 0, fid2 = 0; + uint8_t *pat = NULL, *rd = NULL; + unsigned i; + + setvbuf(stdout, NULL, _IOLBF, 0); + printf("=== test_syssvc_acp_create: executive ACP answers IO$_CREATE/IO$_DELETE/" + "IO$_MODIFY (header alloc + dir insert + dealloc; vms-5303, epic vms-208) ===\n"); + + if (!vms_pcb_init(0xFFFFFFFFFFFFFFFFULL)) { + printf(" FAIL: vms_pcb_init() failed\n"); + return 1; + } + if (!executive_present()) { + printf("=== test_syssvc_acp_create: 0 passed, 0 failed (SKIPPED: no /dev/vms -- " + "IO$_CREATE/DELETE/MODIFY are executive-resident) ===\n"); + return EXIT_SKIP; + } + + pat = malloc(512); + rd = malloc(512); + if (!pat || !rd) { printf(" FAIL: malloc\n"); return 1; } + for (i = 0; i < 512; i++) pat[i] = (uint8_t)(0x3C ^ (i * 7u)); + + st = vms_kif_acp_mount(ODS2_UNIT); + check($VMS_STATUS_SUCCESS(st), "$MOUNT of the genuine ODS-2 " ODS2_UNIT " (precondition)"); + st = vms_kif_acp_assign(ODS2_UNIT, &chan); + check($VMS_STATUS_SUCCESS(st) && chan != 0, "$ASSIGN a file-class channel (precondition)"); + if (chan == 0) { + printf("=== test_syssvc_acp_create: %d passed, %d failed ===\n", pass, fail); + return 1; + } + + /* --- (6) fail-honest: an unknown $QIO function is SS$_BADPARAM ---------- */ + memset(&f, 0, sizeof(f)); + f.chan = chan; f.func = 0xDEAD; + st = vms_kif_acp_fileop(&f); + check(st == SS$_BADPARAM, "IO$_ fileop with an unknown function code is SS$_BADPARAM (fail-honest)"); + + /* --- (6) CREATE into a DID that is not a directory is SS$_NOSUCHFILE ---- */ + memset(&f, 0, sizeof(f)); + f.chan = chan; f.func = VMS_ACP_FOP_CREATE; f.modifiers = VMS_ACP_M_CREATE; + f.did_num = 1; f.did_seq = 1; /* INDEXF.SYS (FID 1) -- not a directory */ + f.kind = ODS2_FK_DATA_FIX; + strncpy(f.name, "NOPE.TST", VMS_ACP_NAME_SIZE - 1); + st = vms_kif_acp_fileop(&f); + check(st == SS$_NOSUCHFILE, "IO$_CREATE into a non-directory DID is SS$_NOSUCHFILE (fail-honest)"); + + /* --- (1) CREATE [OVMXDIR]CREAT.TST -> real FID, dir entry ;1 ------------ */ + st = create_file(chan, "CREAT.TST", 0, VMS_ACP_M_CREATE | VMS_ACP_M_ACCESS, 1, &f); + fid1 = ((uint32_t)f.fid_num) | ((uint32_t)f.fid_nmx << 16); + check($VMS_STATUS_SUCCESS(st) && f.out_version == 1 && fid1 > OVMXDIR_FID_NUM, + "IO$_CREATE CREAT.TST allocates a real FID (a new file number) at version ;1"); + + /* --- (1) the created file is readable back BY NAME ---------------------- */ + (void)vms_kif_acp_deaccess(chan); + /* An off-by-one in the INDEXF header-slot LBN writes the header one slot too + * high, so the FID the directory record resolves to has no valid header and + * the file cannot be re-opened by name. The CREATE itself still returns a + * FID, so ONLY this re-ACCESS-by-name assertion (and its knock-ons) can tell. */ + st = access_named(chan, "CREAT.TST", 0, 0, &a); + /* negctl: acp-create-header-slot-offbyone */ + check($VMS_STATUS_SUCCESS(st) && + (((uint32_t)a.fid_num | ((uint32_t)a.fid_nmx << 16)) == fid1) && + a.out_version == 1, + "IO$_ACCESS CREAT.TST by name resolves the created FID at version 1 (header at its INDEXF slot)"); + (void)vms_kif_acp_deaccess(chan); + + /* --- (2) the created file is a real, writable, DURABLE file ------------- */ + st = access_named(chan, "CREAT.TST", 0, 1, &a); + check($VMS_STATUS_SUCCESS(st), "re-ACCESS CREAT.TST for WRITE"); + memset(&r, 0, sizeof(r)); + r.chan = chan; r.vbn = 1; r.offset = 0; r.length = 512; + r.buffer = (uint64_t)(uintptr_t)pat; + st = vms_kif_acp_writevb(&r); + check($VMS_STATUS_SUCCESS(st) && r.xferred == 512, + "IO$_WRITEVBLK to the freshly created (empty) file extends + writes VBN 1"); + (void)vms_kif_acp_deaccess(chan); + st = access_named(chan, "CREAT.TST", 0, 0, &a); + memset(rd, 0, 512); + memset(&r, 0, sizeof(r)); + r.chan = chan; r.vbn = 1; r.offset = 0; r.length = 512; + r.buffer = (uint64_t)(uintptr_t)rd; + st = vms_kif_acp_readvb(&r); + check($VMS_STATUS_SUCCESS(st) && memcmp(rd, pat, 512) == 0, + "after DEACCESS + re-ACCESS the written bytes persist -- it hit the platter (INV-6)"); + (void)vms_kif_acp_deaccess(chan); + + /* --- (3) CREATE again -> ;2, a second file with a second FID ----------- */ + st = create_file(chan, "CREAT.TST", 0, VMS_ACP_M_CREATE, 0, &f); + fid2 = ((uint32_t)f.fid_num) | ((uint32_t)f.fid_nmx << 16); + check($VMS_STATUS_SUCCESS(st) && f.out_version == 2 && fid2 != 0 && fid2 != fid1, + "IO$_CREATE CREAT.TST again yields version ;2 with a DISTINCT FID"); + st = access_named(chan, "CREAT.TST", 0, 0, &a); /* highest */ + check($VMS_STATUS_SUCCESS(st) && a.out_version == 2 && + (((uint32_t)a.fid_num | ((uint32_t)a.fid_nmx << 16)) == fid2), + "IO$_ACCESS CREAT.TST (highest) now resolves ;2 (versions descending in the dir record)"); + (void)vms_kif_acp_deaccess(chan); + st = access_named(chan, "CREAT.TST", 1, 0, &a); /* explicit ;1 */ + check($VMS_STATUS_SUCCESS(st) && a.out_version == 1 && + (((uint32_t)a.fid_num | ((uint32_t)a.fid_nmx << 16)) == fid1), + "IO$_ACCESS CREAT.TST;1 still resolves the FIRST file (both versions coexist)"); + (void)vms_kif_acp_deaccess(chan); + + /* --- (4) DELETE ;2 removes the entry + deallocates --------------------- */ + st = delete_file(chan, "CREAT.TST", 2, &f); + check($VMS_STATUS_SUCCESS(st), "IO$_DELETE CREAT.TST;2 (remove entry + deallocate header/blocks)"); + st = access_named(chan, "CREAT.TST", 2, 0, &a); + check(st == SS$_NOSUCHFILE, "IO$_ACCESS CREAT.TST;2 after delete is SS$_NOSUCHFILE (entry gone, FID freed)"); + st = access_named(chan, "CREAT.TST", 0, 0, &a); + check($VMS_STATUS_SUCCESS(st) && a.out_version == 1, + "IO$_ACCESS CREAT.TST (highest) after deleting ;2 resolves ;1 again"); + (void)vms_kif_acp_deaccess(chan); + + /* --- (4) DELETE ;1 empties the name ------------------------------------ */ + st = delete_file(chan, "CREAT.TST", 1, &f); + check($VMS_STATUS_SUCCESS(st), "IO$_DELETE CREAT.TST;1 (last version)"); + st = access_named(chan, "CREAT.TST", 0, 0, &a); + check(st == SS$_NOSUCHFILE, "IO$_ACCESS CREAT.TST after deleting every version is SS$_NOSUCHFILE"); + + /* --- (6) DELETE of a name that never existed is SS$_NOSUCHFILE ---------- */ + st = delete_file(chan, "GHOST.TST", 0, &f); + check(st == SS$_NOSUCHFILE, "IO$_DELETE of a nonexistent name is SS$_NOSUCHFILE (fail-honest)"); + + /* --- (5) IO$_MODIFY extend / truncate / write-attributes --------------- */ + st = create_file(chan, "MODF.TST", 0, VMS_ACP_M_CREATE, 0, &f); + check($VMS_STATUS_SUCCESS(st) && f.out_version == 1, "IO$_CREATE MODF.TST for the MODIFY proofs"); + + /* extend by 3 blocks */ + memset(&f, 0, sizeof(f)); + f.chan = chan; f.func = VMS_ACP_FOP_MODIFY; + f.did_num = OVMXDIR_FID_NUM; f.did_seq = 1; f.version = 1; + f.exsz = 3; + strncpy(f.name, "MODF.TST", VMS_ACP_NAME_SIZE - 1); + st = vms_kif_acp_fileop(&f); + check($VMS_STATUS_SUCCESS(st) && f.new_hiblk == 3, + "IO$_MODIFY extend by 3 grows the allocation to HIBLK 3 (BITMAP.SYS alloc + FH2 map append)"); + + /* re-ACCESS confirms the grown allocation persisted */ + st = access_named(chan, "MODF.TST", 0, 0, &a); + check($VMS_STATUS_SUCCESS(st) && a.attr.hiblk == 3, + "after re-ACCESS the on-disk FH2 reports HIBLK 3 (extend persisted)"); + (void)vms_kif_acp_deaccess(chan); + + /* truncate to EOF block 1 */ + memset(&f, 0, sizeof(f)); + f.chan = chan; f.func = VMS_ACP_FOP_MODIFY; + f.did_num = OVMXDIR_FID_NUM; f.did_seq = 1; f.version = 1; + f.trunc_efblk = 1; f.trunc_ffbyte = 0; + strncpy(f.name, "MODF.TST", VMS_ACP_NAME_SIZE - 1); + st = vms_kif_acp_fileop(&f); + check($VMS_STATUS_SUCCESS(st) && f.new_hiblk == 1 && f.new_efblk == 1, + "IO$_MODIFY truncate to EOF block 1 shrinks HIBLK to 1 (blocks past it freed)"); + st = access_named(chan, "MODF.TST", 0, 0, &a); + check($VMS_STATUS_SUCCESS(st) && a.attr.hiblk == 1, + "after re-ACCESS the on-disk FH2 reports the truncated HIBLK 1 (truncate persisted)"); + (void)vms_kif_acp_deaccess(chan); + + /* write attributes: change the protection mask */ + memset(&f, 0, sizeof(f)); + f.chan = chan; f.func = VMS_ACP_FOP_MODIFY; + f.did_num = OVMXDIR_FID_NUM; f.did_seq = 1; f.version = 1; + f.attr_ctl = VMS_ACP_ATTR_PROT; + f.attr.fileprot = 0xFF00u; /* System/Owner keep access; World fully denied */ + strncpy(f.name, "MODF.TST", VMS_ACP_NAME_SIZE - 1); + st = vms_kif_acp_fileop(&f); + check($VMS_STATUS_SUCCESS(st), "IO$_MODIFY write attributes (protection = 0xFF00)"); + st = access_named(chan, "MODF.TST", 0, 0, &a); + check($VMS_STATUS_SUCCESS(st) && a.attr.fileprot == 0xFF00u, + "after re-ACCESS the on-disk FH2 reports the modified protection 0xFF00 (attr write persisted)"); + (void)vms_kif_acp_deaccess(chan); + + /* clean up MODF.TST so the fixture copy is left as found */ + st = delete_file(chan, "MODF.TST", 1, &f); + check($VMS_STATUS_SUCCESS(st), "IO$_DELETE MODF.TST (restore the fixture directory state)"); + + (void)vms_kif_dassgn(chan); + st = vms_kif_acp_dmount(ODS2_UNIT); + check($VMS_STATUS_SUCCESS(st), "$DISMOUNT removes the ODS-2 volume"); + + free(pat); free(rd); + printf("=== test_syssvc_acp_create: %d passed, %d failed ===\n", pass, fail); + return fail > 0 ? 1 : 0; +} From 545508a6347876d82907defebd14df582825ef5a Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 17 Aug 2026 04:01:00 +0000 Subject: [PATCH 002/106] =?UTF-8?q?vms-3e8e:=20IMGACT=20activates=20images?= =?UTF-8?q?=20via=20IO$=5FACCESS+READVBLK=20(ACP=20file=20access,=20ATOMIC?= =?UTF-8?q?-FLIP-GROUP=20=E2=80=94=20red-by-design)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The freestanding image activator now reads every image file over the executive Files-11 (ODS-2) ACP -- $ASSIGN a file-class channel to the mounted volume, IO$_ACCESS the file by walking its directory chain, IO$_READVBLK its header + PT_LOAD segments -- instead of open()/pread()/mmap() on a /vms POSIX path (the passthrough the Files-11 ACP pivot retires, docs/design-files11-acp-executive.md Sec 4.6). Read-then-place first cut; demand-page-through-the-window is the end state (noted as follow-up). - src/imgact/imgact_acp.{c,h}: freestanding ACP reader. Issues REGISTER (adopt- or-create PCB) / ACP_ASSIGN / ACP_ACCESS / ACP_READVBLK / ACP_DEACCESS / DASSGN as raw ioctls on /dev/vms via three host primitives (syscall6 in IMGACT, libc in the test). It calls NO libvmssys vms_kif_* symbol, so no libvmssys_shr.vec / SYS_VEC / native-link enumeration change is needed. - src/imgact/imgact.c: load_object, load_ovmx_producer, ovmx_find_section, apply_vms_rel and activate_symbol_vector read through an imgsrc handle backed by the ACP. NO silent POSIX fallback (INV-6): no /dev/vms or file-not-on-the- ACP-volume -> honest %IMGACT-F-IMGNOTFND, never a /vms read. SYS_ioctl added per arch. - tests/qemu: test_syssvc_imgact_acp.c drives the exact freestanding reader against a real /dev/vms over a generated ODS-2 fixture (mkimage_ods2_imgact.c + imgact_acp_fixture_elf.h) on a new 4th disk DKA300: (vdd). test_kmod_disk updated for the 4th disk (negctl moves to DKA400:). Genuine negctl anchor imgact-acp-valid-bytes-offbyone in facility_defects.sh (FLOOR-NO-BUMP). Proven on real /dev/vms (QEMU kernel-executive harness): test_syssvc_imgact_acp 13 passed, 0 failed -- header + program-header table + every PT_LOAD segment + whole image byte-exact vs the on-disk image, fail-honest SS$_NOSUCHFILE / SS$_NOSUCHDEV. test_kmod_disk 19 passed, 0 failed. ATOMIC-FLIP-GROUP member, red-by-design until the flip ACP-mounts SYS$DISK. DOES NOT MERGE STANDALONE. See the PR body for the expected-red inventory. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/imgact/CMakeLists.txt | 9 +- src/imgact/Makefile | 7 +- src/imgact/arch/aarch64/imgact_arch.h | 1 + src/imgact/arch/alpha/imgact_arch.h | 1 + src/imgact/arch/x86_64/imgact_arch.h | 1 + src/imgact/imgact.c | 171 +++++++++---- src/imgact/imgact_acp.c | 339 ++++++++++++++++++++++++++ src/imgact/imgact_acp.h | 93 +++++++ tests/qemu/CMakeLists.txt | 15 ++ tests/qemu/Dockerfile | 16 ++ tests/qemu/facility_defects.sh | 34 ++- tests/qemu/imgact_acp_fixture_elf.h | 116 +++++++++ tests/qemu/mkimage_ods2_imgact.c | 168 +++++++++++++ tests/qemu/run_tests.sh | 30 ++- tests/qemu/test_kmod_disk.c | 29 ++- tests/qemu/test_syssvc_imgact_acp.c | 235 ++++++++++++++++++ 16 files changed, 1196 insertions(+), 69 deletions(-) create mode 100644 src/imgact/imgact_acp.c create mode 100644 src/imgact/imgact_acp.h create mode 100644 tests/qemu/imgact_acp_fixture_elf.h create mode 100644 tests/qemu/mkimage_ods2_imgact.c create mode 100644 tests/qemu/test_syssvc_imgact_acp.c diff --git a/src/imgact/CMakeLists.txt b/src/imgact/CMakeLists.txt index 4e74e382e..e268e12c9 100644 --- a/src/imgact/CMakeLists.txt +++ b/src/imgact/CMakeLists.txt @@ -29,9 +29,15 @@ set(IMGACT_ARCHDIR "${CMAKE_CURRENT_SOURCE_DIR}/arch/${IMGACT_ARCH}") # provides the libc shim (open/fstat/mmap/munmap/close/strncpy/strncmp) its # calls need. Wires known_images_lookup() into the DT_NEEDED search path # (bead vms-30d). +# imgact_acp.c (bead vms-3e8e) is compiled directly as an extra source of THIS +# freestanding target, the same way known_images.c is: it reads images over the +# executive Files-11 ACP (IO$_ACCESS + IO$_READVBLK on /dev/vms) instead of +# open()/pread() on a /vms POSIX path. imgact.c provides its three host +# primitives (imgact_acp_dev_open/close/ioctl) on the raw syscall layer. add_executable(imgact imgact.c known_images.c + imgact_acp.c ${IMGACT_ARCHDIR}/start.S ) @@ -42,7 +48,8 @@ add_executable(imgact # -I../libvms/include) so the CMake OVMX_IMGACT build sees the same headers. target_include_directories(imgact PRIVATE ${CMAKE_SOURCE_DIR}/src/vmslink/include - ${CMAKE_SOURCE_DIR}/src/libvms/include) + ${CMAKE_SOURCE_DIR}/src/libvms/include + ${CMAKE_SOURCE_DIR}/src/kernel) # vms_ioctl.h + vms_acp.h (ACP arg structs) target_compile_options(imgact PRIVATE $<$:-std=gnu11> diff --git a/src/imgact/Makefile b/src/imgact/Makefile index d0c6db1e6..3ddff6ec1 100644 --- a/src/imgact/Makefile +++ b/src/imgact/Makefile @@ -19,6 +19,7 @@ ARCHDIR := arch/$(ARCH) IMGACT_CFLAGS := -std=gnu11 -O2 -Wall -Wextra \ -I../vmslink/include \ -I../libvms/include \ + -I../kernel \ -fPIC -fvisibility=hidden -ffreestanding \ -fno-stack-protector -fno-builtin -fno-asynchronous-unwind-tables IMGACT_LDFLAGS := -nostdlib -nostartfiles -shared \ @@ -28,13 +29,13 @@ IMGACT_LDFLAGS := -nostdlib -nostartfiles -shared \ # translation unit here so IMGACT.EXE can call known_images_lookup() (bead # vms-30d wiring); imgact.c provides the open/fstat/mmap/munmap/close/ # strncpy/strncmp shim its libc calls need under -nostdlib. -IMGACT_SRC := imgact.c known_images.c $(ARCHDIR)/start.S +IMGACT_SRC := imgact.c known_images.c imgact_acp.c $(ARCHDIR)/start.S .PHONY: all clean all: IMGACT.EXE -IMGACT.EXE: $(IMGACT_SRC) $(ARCHDIR)/imgact_arch.h known_images.h - $(CC) $(IMGACT_CFLAGS) $(IMGACT_LDFLAGS) -o $@ imgact.c known_images.c $(ARCHDIR)/start.S +IMGACT.EXE: $(IMGACT_SRC) $(ARCHDIR)/imgact_arch.h known_images.h imgact_acp.h + $(CC) $(IMGACT_CFLAGS) $(IMGACT_LDFLAGS) -o $@ imgact.c known_images.c imgact_acp.c $(ARCHDIR)/start.S clean: rm -f IMGACT.EXE test/*.o test/test_prog 'test/LIBTEST$$SHR.EXE' diff --git a/src/imgact/arch/aarch64/imgact_arch.h b/src/imgact/arch/aarch64/imgact_arch.h index b20708990..9b0741d95 100644 --- a/src/imgact/arch/aarch64/imgact_arch.h +++ b/src/imgact/arch/aarch64/imgact_arch.h @@ -22,6 +22,7 @@ #define SYS_close 57 #define SYS_read 63 #define SYS_pread64 67 +#define SYS_ioctl 29 #define SYS_write 64 #define SYS_mmap 222 #define SYS_mprotect 226 diff --git a/src/imgact/arch/alpha/imgact_arch.h b/src/imgact/arch/alpha/imgact_arch.h index 8c0966626..edcee4ad8 100644 --- a/src/imgact/arch/alpha/imgact_arch.h +++ b/src/imgact/arch/alpha/imgact_arch.h @@ -34,6 +34,7 @@ #define SYS_close 6 #define SYS_read 3 #define SYS_pread64 349 +#define SYS_ioctl 54 #define SYS_write 4 #define SYS_mmap 71 #define SYS_mprotect 74 diff --git a/src/imgact/arch/x86_64/imgact_arch.h b/src/imgact/arch/x86_64/imgact_arch.h index 877cf4330..f43f3cc5e 100644 --- a/src/imgact/arch/x86_64/imgact_arch.h +++ b/src/imgact/arch/x86_64/imgact_arch.h @@ -25,6 +25,7 @@ #define SYS_close 3 #define SYS_read 0 #define SYS_pread64 17 +#define SYS_ioctl 16 #define SYS_write 1 #define SYS_mmap 9 #define SYS_mprotect 10 diff --git a/src/imgact/imgact.c b/src/imgact/imgact.c index 6f375b51c..03f90da33 100644 --- a/src/imgact/imgact.c +++ b/src/imgact/imgact.c @@ -46,6 +46,7 @@ #include "ovmx_symvec.h" /* shared resolver + GSMATCH (bead vms-8d5) */ #include "known_images.h" /* Known Image DB lookup (bead vms-913.5; wired vms-30d) */ #include "imgact_prodreg.h" /* publish resident producers into LIBVMS$SHR (vms-db2) */ +#include "imgact_acp.h" /* image reads over the executive Files-11 ACP (vms-3e8e) */ #ifndef AT_EXECFN #define AT_EXECFN 31 @@ -54,6 +55,18 @@ #ifndef O_RDONLY #define O_RDONLY 0 #endif +#ifndef O_RDWR +#define O_RDWR 2 +#endif + +/* The system disk the activator reads images from. On real OpenVMS this is the + * discovered SYS$SYSDEVICE; IMGACT hardcodes the boot unit exactly as it used + * to hardcode the "/vms" mount point (IMGACT_FALLBACK_SYSLIB). Resolving + * SYS$SYSDEVICE dynamically is a follow-up (it is a discovered logical, epic + * vms-47d). The QEMU harness maps the boot volume to DKA0:. */ +#ifndef IMGACT_ACP_SYSDEVICE +#define IMGACT_ACP_SYSDEVICE "DKA0:" +#endif /* -------------------------------------------------------------------------- * Freestanding syscall layer (no libc; IMGACT.EXE is -nostdlib). @@ -68,10 +81,10 @@ static long sys_openat(const char *path, int flags) return syscall6(SYS_openat, -100, (long)path, flags, 0, 0, 0); } static long sys_close(int fd) { return syscall6(SYS_close, fd, 0, 0, 0, 0, 0); } -static long sys_pread(int fd, void *buf, unsigned long n, long off) -{ - return syscall6(SYS_pread64, fd, (long)buf, n, off, 0, 0); -} +/* NOTE (vms-3e8e): image-file reads no longer use pread(2) on a /vms POSIX + * path -- they ride the executive Files-11 ACP (IO$_READVBLK, imgact_acp.c). + * SYS_pread64 stays defined for the arch headers' completeness; there is no + * sys_pread() wrapper because nothing in the activator reads a file that way. */ static long sys_write(int fd, const void *buf, unsigned long n) { return syscall6(SYS_write, fd, (long)buf, n, 0, 0, 0); @@ -94,6 +107,29 @@ static long sys_munmap(void *addr, unsigned long len) { return syscall6(SYS_munmap, (long)addr, len, 0, 0, 0, 0); } +static long sys_ioctl(int fd, unsigned long req, void *arg) +{ + return syscall6(SYS_ioctl, fd, (long)req, (long)arg, 0, 0, 0); +} + +/* -------------------------------------------------------------------------- + * Files-11 ACP host primitives (vms-3e8e). imgact_acp.c reaches /dev/vms + * through these three functions; here they are the freestanding syscall + * backings (the QEMU test provides libc-backed versions of the same symbols). + * -------------------------------------------------------------------------- */ +int imgact_acp_dev_open(void) +{ + long fd = sys_openat("/dev/vms", O_RDWR); + return fd < 0 ? -1 : (int)fd; +} +void imgact_acp_dev_close(int fd) +{ + sys_close(fd); +} +long imgact_acp_dev_ioctl(int fd, unsigned long req, void *arg) +{ + return sys_ioctl(fd, req, arg); +} #ifndef PROT_READ #define PROT_READ 0x1 @@ -557,32 +593,59 @@ static void scan_tls(struct obj *o, Elf64_Phdr *phdr, int phnum) } } +/* -------------------------------------------------------------------------- + * Image source (vms-3e8e): every image-file read the activator does now rides + * the executive Files-11 (ODS-2) ACP -- IO$_ACCESS + IO$_READVBLK over + * /dev/vms (imgact_acp.c) -- NOT open()/pread() on a /vms POSIX path, the + * passthrough the ACP pivot retires (docs/design-files11-acp-executive.md + * §4.6). Fail-honest: if the boot volume is not ACP-mounted (SS$_NOSUCHDEV) or + * the image is not on it (SS$_NOSUCHFILE) the open fails and the activator dies + * with an honest %IMGACT message; there is NEVER a silent POSIX fallback + * (CLAUDE.md Rule 9 / INV-6). imgsrc_pread() keeps pread(2) semantics so the + * existing exact-count read checks below are unchanged. + * -------------------------------------------------------------------------- */ +struct imgsrc { struct imgact_acp_file f; }; + +static int imgsrc_open(struct imgsrc *s, const char *path) +{ + uint32_t st = imgact_acp_open(&s->f, IMGACT_ACP_SYSDEVICE, path); + return (st & 1u) ? 0 : -1; +} +static long imgsrc_pread(struct imgsrc *s, void *buf, unsigned long n, long off) +{ + return imgact_acp_pread(&s->f, buf, n, off); +} +static void imgsrc_close(struct imgsrc *s) +{ + imgact_acp_close(&s->f); +} + /* Map a shareable image file into memory; fill base/dyn. Returns obj index. */ static struct obj *load_object(const char *soname, const char *path) { if (g_nobjs >= MAX_OBJS) die_mapfail(soname); - int fd = (int)sys_openat(path, O_RDONLY); - if (fd < 0) + struct imgsrc src; + if (imgsrc_open(&src, path) < 0) return 0; Elf64_Ehdr eh; - if (sys_pread(fd, &eh, sizeof(eh), 0) != (long)sizeof(eh) || + if (imgsrc_pread(&src, &eh, sizeof(eh), 0) != (long)sizeof(eh) || eh.e_ident[0] != 0x7f || eh.e_ident[1] != 'E' || eh.e_ident[2] != 'L' || eh.e_ident[3] != 'F') { - sys_close(fd); + imgsrc_close(&src); die_imgfmterr(soname); } Elf64_Phdr ph[32]; if (eh.e_phnum > 32) { - sys_close(fd); + imgsrc_close(&src); die_imgfmterr(soname); } - if (sys_pread(fd, ph, (unsigned long)eh.e_phnum * eh.e_phentsize, + if (imgsrc_pread(&src, ph, (unsigned long)eh.e_phnum * eh.e_phentsize, (long)eh.e_phoff) < 0) { - sys_close(fd); + imgsrc_close(&src); die_imgfmterr(soname); } @@ -597,17 +660,19 @@ static struct obj *load_object(const char *soname, const char *path) hi = ph[i].p_vaddr + ph[i].p_memsz; } if (lo == ~0UL) { - sys_close(fd); + imgsrc_close(&src); die_imgfmterr(soname); } unsigned long span = PAGE_UP(hi) - lo; - /* Reserve the whole span R/W anonymous, then read segments into place. - * Anonymous memory is zero-filled, so .bss needs no explicit clear. */ + /* Reserve the whole span R/W anonymous, then read segments into place + * via the ACP window (IO$_READVBLK). Anonymous memory is zero-filled, + * so .bss needs no explicit clear. (Read-then-place first cut, vms-3e8e; + * demand-page-through-the-window is the end state.) */ void *map = sys_mmap(0, span, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (map == MAP_FAILED) { - sys_close(fd); + imgsrc_close(&src); die_mapfail(soname); } unsigned long base = (unsigned long)map - lo; @@ -615,13 +680,13 @@ static struct obj *load_object(const char *soname, const char *path) for (int i = 0; i < eh.e_phnum; i++) { if (ph[i].p_type != PT_LOAD || ph[i].p_filesz == 0) continue; - if (sys_pread(fd, (void *)(base + ph[i].p_vaddr), + if (imgsrc_pread(&src, (void *)(base + ph[i].p_vaddr), ph[i].p_filesz, (long)ph[i].p_offset) < 0) { - sys_close(fd); + imgsrc_close(&src); die_mapfail(soname); } } - sys_close(fd); + imgsrc_close(&src); /* Apply final segment protections. */ for (int i = 0; i < eh.e_phnum; i++) { @@ -1026,24 +1091,25 @@ static unsigned long exec_bias(Elf64_Phdr *phdr, int phnum, unsigned long at_phd * DT_HASH/DT_NEEDED resolution. * -------------------------------------------------------------------------- */ -/* Find a section's load vaddr + size by name, via the file's section headers. */ -static int ovmx_find_section(int fd, const char *want, +/* Find a section's load vaddr + size by name, via the file's section headers + * (read over the ACP window, vms-3e8e). */ +static int ovmx_find_section(struct imgsrc *src, const char *want, unsigned long *addr, unsigned long *size) { Elf64_Ehdr eh; - if (sys_pread(fd, &eh, sizeof eh, 0) != (long)sizeof eh) + if (imgsrc_pread(src, &eh, sizeof eh, 0) != (long)sizeof eh) return 0; if (eh.e_shnum == 0 || eh.e_shnum > 64 || eh.e_shstrndx >= eh.e_shnum) return 0; Elf64_Shdr sh[64]; unsigned long ssz = (unsigned long)eh.e_shnum * sizeof(Elf64_Shdr); - if (sys_pread(fd, sh, ssz, (long)eh.e_shoff) != (long)ssz) + if (imgsrc_pread(src, sh, ssz, (long)eh.e_shoff) != (long)ssz) return 0; static char strtab[2048]; unsigned long stsz = sh[eh.e_shstrndx].sh_size; if (stsz > sizeof strtab) return 0; - if (sys_pread(fd, strtab, stsz, (long)sh[eh.e_shstrndx].sh_offset) != (long)stsz) + if (imgsrc_pread(src, strtab, stsz, (long)sh[eh.e_shstrndx].sh_offset) != (long)stsz) return 0; for (int i = 0; i < eh.e_shnum; i++) { if (xstrcmp(strtab + sh[i].sh_name, want) == 0) { @@ -1058,12 +1124,12 @@ static int ovmx_find_section(int fd, const char *want, /* Apply the .vms$rel self-relative fixups: add the load bias to every * image-relative slot LINK.EXE recorded (synthesized GOT cells, pointer data). * The VMS-native equivalent of processing R_AARCH64_RELATIVE, without a - * PT_DYNAMIC. No-op for images with no .vms$rel. `fd` must be open on the image; - * `base` is its load bias. The target pages must already be writable. */ -static void apply_vms_rel(int fd, unsigned long base) + * PT_DYNAMIC. No-op for images with no .vms$rel. `src` must be open on the + * image; `base` is its load bias. The target pages must already be writable. */ +static void apply_vms_rel(struct imgsrc *src, unsigned long base) { unsigned long rel_addr, rel_size; - if (!ovmx_find_section(fd, OVMX_REL_SECTION, &rel_addr, &rel_size)) + if (!ovmx_find_section(src, OVMX_REL_SECTION, &rel_addr, &rel_size)) return; const struct ovmx_rel_header *rh = (const struct ovmx_rel_header *)(base + rel_addr); @@ -1114,22 +1180,21 @@ static struct ovmx_prod *load_ovmx_producer(const char *soname) if (g_nprods >= 32) return 0; - /* Search: SYS$SHARE fallback, then the name as given. */ + /* Search: SYS$SHARE fallback, then the name as given -- both read over + * the executive Files-11 ACP (vms-3e8e), never a /vms POSIX open. */ char path[256]; xstrcpy(path, IMGACT_FALLBACK_SYSLIB "/"); xstrcat(path, soname); - int fd = (int)sys_openat(path, O_RDONLY); - if (fd < 0) - fd = (int)sys_openat(soname, O_RDONLY); - if (fd < 0) + struct imgsrc src; + if (imgsrc_open(&src, path) < 0 && imgsrc_open(&src, soname) < 0) return 0; Elf64_Ehdr eh; - if (sys_pread(fd, &eh, sizeof eh, 0) != (long)sizeof eh) { sys_close(fd); return 0; } + if (imgsrc_pread(&src, &eh, sizeof eh, 0) != (long)sizeof eh) { imgsrc_close(&src); return 0; } Elf64_Phdr ph[16]; - if (eh.e_phnum > 16) { sys_close(fd); return 0; } - if (sys_pread(fd, ph, (unsigned long)eh.e_phnum * sizeof(Elf64_Phdr), - (long)eh.e_phoff) < 0) { sys_close(fd); return 0; } + if (eh.e_phnum > 16) { imgsrc_close(&src); return 0; } + if (imgsrc_pread(&src, ph, (unsigned long)eh.e_phnum * sizeof(Elf64_Phdr), + (long)eh.e_phoff) < 0) { imgsrc_close(&src); return 0; } unsigned long lo = ~0UL, hi = 0; for (int i = 0; i < eh.e_phnum; i++) { @@ -1137,16 +1202,16 @@ static struct ovmx_prod *load_ovmx_producer(const char *soname) if (PAGE_DOWN(ph[i].p_vaddr) < lo) lo = PAGE_DOWN(ph[i].p_vaddr); if (ph[i].p_vaddr + ph[i].p_memsz > hi) hi = ph[i].p_vaddr + ph[i].p_memsz; } - if (lo == ~0UL) { sys_close(fd); return 0; } + if (lo == ~0UL) { imgsrc_close(&src); return 0; } unsigned long span = PAGE_UP(hi) - lo; void *map = sys_mmap(0, span, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - if (map == MAP_FAILED) { sys_close(fd); return 0; } + if (map == MAP_FAILED) { imgsrc_close(&src); return 0; } unsigned long base = (unsigned long)map - lo; for (int i = 0; i < eh.e_phnum; i++) { if (ph[i].p_type != PT_LOAD || ph[i].p_filesz == 0) continue; - if (sys_pread(fd, (void *)(base + ph[i].p_vaddr), ph[i].p_filesz, - (long)ph[i].p_offset) < 0) { sys_close(fd); return 0; } + if (imgsrc_pread(&src, (void *)(base + ph[i].p_vaddr), ph[i].p_filesz, + (long)ph[i].p_offset) < 0) { imgsrc_close(&src); return 0; } } for (int i = 0; i < eh.e_phnum; i++) { if (ph[i].p_type != PT_LOAD) continue; @@ -1160,20 +1225,20 @@ static struct ovmx_prod *load_ovmx_producer(const char *soname) } unsigned long sv_addr, sv_size; - int ok = ovmx_find_section(fd, OVMX_SV_SECTION, &sv_addr, &sv_size); + int ok = ovmx_find_section(&src, OVMX_SV_SECTION, &sv_addr, &sv_size); /* Bias this producer's own self-relative slots (GOT cells, pointer data) * before any of its universal code runs. Pages are RWX at this point. */ if (ok) - apply_vms_rel(fd, base); + apply_vms_rel(&src, base); /* Locate the .vms$tls TLSDESC table (completed after TLS offsets assigned). */ unsigned long tls_addr, tls_size; - int have_tlsdesc = ovmx_find_section(fd, OVMX_TLS_SECTION, &tls_addr, &tls_size); + int have_tlsdesc = ovmx_find_section(&src, OVMX_TLS_SECTION, &tls_addr, &tls_size); /* Locate this producer's OWN .vms$imp: a lib shareable that itself imports * from another producer (e.g. libc/pthread from DECC$SHR). Resolved * transitively after registration below. (vms-e65) */ unsigned long imp_addr, imp_size; - int have_imp = ovmx_find_section(fd, OVMX_IMP_SECTION, &imp_addr, &imp_size); - sys_close(fd); + int have_imp = ovmx_find_section(&src, OVMX_IMP_SECTION, &imp_addr, &imp_size); + imgsrc_close(&src); if (!ok) return 0; @@ -1507,15 +1572,19 @@ static void activate_symbol_vector(unsigned long exe_base, const char *execfn, { if (!execfn) die_imgfmterr("IMAGE.EXE"); - int fd = (int)sys_openat(execfn, O_RDONLY); - if (fd < 0) + /* The main image's LOAD segments are already kernel-mapped; its SECTION + * headers (.vms$imp/.vms$rel/.vms$tls) are not, so re-read them off the + * file -- now over the executive Files-11 ACP (vms-3e8e), not a /vms POSIX + * open. Fail-honest: not on the ACP volume -> honest %IMGACT error. */ + struct imgsrc src; + if (imgsrc_open(&src, execfn) < 0) die_imgnotfnd(execfn); unsigned long imp_addr, imp_size; - int ok = ovmx_find_section(fd, OVMX_IMP_SECTION, &imp_addr, &imp_size); + int ok = ovmx_find_section(&src, OVMX_IMP_SECTION, &imp_addr, &imp_size); /* Bias the executable's own self-relative slots (its GOT/pointer data), if * any; harmless no-op for the current PLT-only executables. */ if (ok) - apply_vms_rel(fd, exe_base); + apply_vms_rel(&src, exe_base); /* Record the executable module's OWN TLS geometry (PT_TLS) + its .vms$tls * TLSDESC table, so the executable participates in TLS setup exactly like a @@ -1534,11 +1603,11 @@ static void activate_symbol_vector(unsigned long exe_base, const char *execfn, } if (g_exe.has_tls) { unsigned long tls_addr, tls_size; - if (ovmx_find_section(fd, OVMX_TLS_SECTION, &tls_addr, &tls_size)) + if (ovmx_find_section(&src, OVMX_TLS_SECTION, &tls_addr, &tls_size)) g_exe.tlsdesc = (const struct ovmx_tls_header *)(exe_base + tls_addr); } - sys_close(fd); + imgsrc_close(&src); if (!ok) die_imgfmterr("IMAGE.EXE"); diff --git a/src/imgact/imgact_acp.c b/src/imgact/imgact_acp.c new file mode 100644 index 000000000..a0ec899ee --- /dev/null +++ b/src/imgact/imgact_acp.c @@ -0,0 +1,339 @@ +/* + * imgact_acp.c - IMGACT image reads over the executive Files-11 (ODS-2) ACP. + * + * See imgact_acp.h for the model and the no-POSIX-fallback (INV-6) contract. + * + * This translation unit is compiled BOTH into the freestanding IMGACT.EXE (as + * an extra source, exactly like known_images.c) and into the hosted QEMU test + * (tests/qemu/test_syssvc_imgact_acp.c). It therefore uses only its own static + * memory helpers and the three host primitives declared in imgact_acp.h -- no + * libc, no imgact.c internals -- so the same code runs in both worlds. + * + * CLEAN-ROOM (CLAUDE.md Rule 8). The ACP ioctl arg structs / request numbers + * are OVMX design choices labelled as such in src/kernel/vms_acp.h; this file + * only marshals into them. It reads no OpenVMS byte-level wire. + */ + +#include + +/* One include pulls in the _IOWR encoding, VMS_IOC_MAGIC, VMS_DEVNAM_SIZE, and + * every ACP / register / dassgn arg struct + request number (vms_ioctl.h + * includes vms_lnm.h, vms_mbx.h and vms_acp.h at its foot). */ +#include "vms_ioctl.h" +#include "ssdef.h" + +#include "imgact_acp.h" + +/* -------------------------------------------------------------------------- + * Local, self-contained helpers (no libc; usable in the freestanding link). + * -------------------------------------------------------------------------- */ + +static void acp_memset(void *d, int c, unsigned long n) +{ + unsigned char *p = d; + while (n--) + *p++ = (unsigned char)c; +} + +static unsigned long acp_strlen(const char *s) +{ + const char *p = s; + while (*p) + p++; + return (unsigned long)(p - s); +} + +/* Copy at most cap-1 bytes of NUL-terminated src into dst; always NUL-term. */ +static void acp_strlcpy(char *dst, const char *src, unsigned long cap) +{ + unsigned long i = 0; + if (cap == 0) + return; + for (; i + 1 < cap && src[i]; i++) + dst[i] = src[i]; + dst[i] = '\0'; +} + +/* -------------------------------------------------------------------------- + * ACP primitives -- one thin wrapper per ioctl the activator needs. + * -------------------------------------------------------------------------- */ + +/* Register (adopt-or-create) this process's executive PCB. The executive keys + * the PCB on the thread group and it survives execve, so when IMGACT runs as + * the interpreter of an image DCL activated (register-continue), this ADOPTS + * the existing row (vms_module.c: "register a process that already exists -> + * hand back the process that already exists"); in a fresh process (the test) + * it creates one. Either way the ACP ioctls that follow have a process. */ +static uint32_t acp_register(int fd) +{ + struct vms_register_args a; + acp_memset(&a, 0, sizeof(a)); + if (imgact_acp_dev_ioctl(fd, VMS_IOCTL_REGISTER, &a) < 0) + return SS$_NOSUCHDEV; + return a.status; +} + +/* $ASSIGN a FILE-CLASS channel to the mounted ODS-2 volume via the DEDICATED + * ACP assign ioctl (VMS_IOCTL_ACP_ASSIGN, 0x6A) -- the same one vms_kif_acp_ + * assign() issues. The generic VMS_IOCTL_ASSIGN (0x50) is NOT equivalent: the + * "$ASSIGN of a mounted volume routes to the ACP" behaviour lives in userspace + * sys_assign.c, which this freestanding path does not run, so it must name the + * ACP assign directly or the channel is not file-class and IO$_ACCESS on it + * fails. SS$_NOSUCHDEV when the unit is not an ACP-mounted volume (fail-honest). */ +static uint32_t acp_assign(int fd, const char *dev, uint32_t *chan) +{ + struct vms_acp_assign_args a; + acp_memset(&a, 0, sizeof(a)); + acp_strlcpy(a.devnam, dev, sizeof(a.devnam)); + if (imgact_acp_dev_ioctl(fd, VMS_IOCTL_ACP_ASSIGN, &a) < 0) + return SS$_NOSUCHDEV; + if ($VMS_STATUS_SUCCESS(a.status)) + *chan = a.chan; + return a.status; +} + +static void acp_dassgn(int fd, uint32_t chan) +{ + struct vms_dassgn_args a; + acp_memset(&a, 0, sizeof(a)); + a.chan = chan; + (void)imgact_acp_dev_ioctl(fd, VMS_IOCTL_DASSGN, &a); +} + +static void acp_deaccess(int fd, uint32_t chan) +{ + struct vms_acp_deaccess_args a; + acp_memset(&a, 0, sizeof(a)); + a.chan = chan; + (void)imgact_acp_dev_ioctl(fd, VMS_IOCTL_ACP_DEACCESS, &a); +} + +/* + * IO$_ACCESS one path component on `chan`. `name` is "NAME.TYPE" (a directory + * is "NAME.DIR"); `did_*` is the directory to search (0,0,0 => the MFD). On + * success returns SS$_NORMAL and fills *out_fid_* + the file geometry + * (*out_valid = total valid bytes; 0 for a directory, which the caller does + * not read). Leaves the file accessed on the channel (the caller deaccesses). + */ +static uint32_t acp_access_name(int fd, uint32_t chan, const char *name, + uint16_t did_num, uint16_t did_seq, + uint8_t did_rvn, uint8_t did_nmx, + uint16_t *out_fid_num, uint16_t *out_fid_seq, + uint8_t *out_fid_rvn, uint8_t *out_fid_nmx, + uint32_t *out_valid) +{ + struct vms_acp_access_args a; + + acp_memset(&a, 0, sizeof(a)); + a.chan = chan; + a.did_num = did_num; + a.did_seq = did_seq; + a.did_rvn = did_rvn; + a.did_nmx = did_nmx; + acp_strlcpy(a.name, name, sizeof(a.name)); + + if (imgact_acp_dev_ioctl(fd, VMS_IOCTL_ACP_ACCESS, &a) < 0) + return SS$_NOSUCHDEV; + + if ($VMS_STATUS_SUCCESS(a.status)) { + if (out_fid_num) *out_fid_num = a.fid_num; + if (out_fid_seq) *out_fid_seq = a.fid_seq; + if (out_fid_rvn) *out_fid_rvn = a.fid_rvn; + if (out_fid_nmx) *out_fid_nmx = a.fid_nmx; + if (out_valid) { + uint32_t efblk = a.attr.efblk; + *out_valid = efblk + ? (efblk - 1u) * 512u + a.attr.ffbyte + : 0u; + } + } + return a.status; +} + +/* -------------------------------------------------------------------------- + * Path parsing: pull the next '/'-separated component out of *pp into buf, + * advancing *pp. Returns the component length, or 0 when the path is + * exhausted. A leading "/vms" (the retired POSIX mount point) and empty + * components (leading/duplicate slashes) are skipped by the caller loop. + * -------------------------------------------------------------------------- */ +static unsigned long next_component(const char **pp, char *buf, unsigned long cap) +{ + const char *p = *pp; + unsigned long n = 0; + + while (*p == '/') + p++; + while (*p && *p != '/') { + if (n + 1 < cap) + buf[n] = *p; + n++; + p++; + } + buf[n < cap ? n : cap - 1] = '\0'; + *pp = p; + return n; +} + +/* -------------------------------------------------------------------------- + * Public API. + * -------------------------------------------------------------------------- */ + +uint32_t imgact_acp_open(struct imgact_acp_file *f, const char *dev, + const char *path) +{ + uint32_t st; + int fd; + const char *p = path; + char comp[VMS_ACP_NAME_SIZE]; + char next[VMS_ACP_NAME_SIZE]; + uint16_t did_num = 0, did_seq = 0; + uint8_t did_rvn = 0, did_nmx = 0; + + acp_memset(f, 0, sizeof(*f)); + f->dev_fd = -1; + + fd = imgact_acp_dev_open(); + if (fd < 0) + return SS$_NOSUCHDEV; + f->dev_fd = fd; + + st = acp_register(fd); + if (!$VMS_STATUS_SUCCESS(st)) { + imgact_acp_dev_close(fd); + f->dev_fd = -1; + return st; + } + + st = acp_assign(fd, dev, &f->chan); + if (!$VMS_STATUS_SUCCESS(st)) { + imgact_acp_dev_close(fd); + f->dev_fd = -1; + return st; + } + + /* Strip a leading "/vms" mount-point component if present. */ + { + const char *q = p; + while (*q == '/') + q++; + if ((q[0] == 'v' || q[0] == 'V') && + (q[1] == 'm' || q[1] == 'M') && + (q[2] == 's' || q[2] == 'S') && + (q[3] == '/' || q[3] == '\0')) + p = q + 3; + } + + /* Pull the first real component; each subsequent one is looked ahead so + * the LAST component is opened as a file and the rest as directories. */ + if (next_component(&p, comp, sizeof(comp)) == 0) { + acp_dassgn(fd, f->chan); + imgact_acp_dev_close(fd); + f->dev_fd = -1; + return SS$_NOSUCHFILE; + } + + for (;;) { + int has_next = (next_component(&p, next, sizeof(next)) != 0); + + if (!has_next) { + /* Final component: the image file itself. Leave it + * accessed on the channel for imgact_acp_pread(). */ + st = acp_access_name(fd, f->chan, comp, + did_num, did_seq, did_rvn, did_nmx, + 0, 0, 0, 0, &f->valid); + if (!$VMS_STATUS_SUCCESS(st)) { + acp_dassgn(fd, f->chan); + imgact_acp_dev_close(fd); + f->dev_fd = -1; + return st; + } + f->accessed = 1; + return st; + } + + /* Intermediate component: an ODS-2 directory "NAME.DIR". Resolve + * its FID, then DID-chain to it for the next level. We only need + * the FID, so release the directory before descending. */ + { + char dirname[VMS_ACP_NAME_SIZE]; + uint16_t fnum = 0, fseq = 0; + uint8_t frvn = 0, fnmx = 0; + unsigned long L = acp_strlen(comp); + + acp_strlcpy(dirname, comp, sizeof(dirname)); + if (L + 4 < sizeof(dirname)) { + dirname[L + 0] = '.'; + dirname[L + 1] = 'D'; + dirname[L + 2] = 'I'; + dirname[L + 3] = 'R'; + dirname[L + 4] = '\0'; + } + + st = acp_access_name(fd, f->chan, dirname, + did_num, did_seq, did_rvn, did_nmx, + &fnum, &fseq, &frvn, &fnmx, 0); + if (!$VMS_STATUS_SUCCESS(st)) { + acp_dassgn(fd, f->chan); + imgact_acp_dev_close(fd); + f->dev_fd = -1; + return st; + } + acp_deaccess(fd, f->chan); + + did_num = fnum; + did_seq = fseq; + did_rvn = frvn; + did_nmx = fnmx; + } + + /* Advance: the looked-ahead component becomes the current one. */ + acp_strlcpy(comp, next, sizeof(comp)); + } +} + +long imgact_acp_pread(struct imgact_acp_file *f, void *buf, + unsigned long n, long off) +{ + struct vms_acp_rw_args r; + unsigned long avail; + + if (!f || !f->accessed || off < 0) + return -1; + if ((unsigned long)off >= f->valid) + return 0; /* at/after EOF -> 0 bytes */ + + avail = f->valid - (unsigned long)off; + if (n > avail) + n = avail; /* clamp; readvb never over-reads */ + if (n == 0) + return 0; + + acp_memset(&r, 0, sizeof(r)); + r.chan = f->chan; + r.vbn = (uint32_t)((unsigned long)off / 512u) + 1u; + r.offset = (uint32_t)((unsigned long)off % 512u); + r.length = (uint32_t)n; + r.buffer = (uint64_t)(uintptr_t)buf; + + if (imgact_acp_dev_ioctl(f->dev_fd, VMS_IOCTL_ACP_READVBLK, &r) < 0) + return -1; + if (r.status == SS$_ENDOFFILE) + return 0; + if (!$VMS_STATUS_SUCCESS(r.status)) + return -1; + return (long)r.xferred; +} + +void imgact_acp_close(struct imgact_acp_file *f) +{ + if (!f || f->dev_fd < 0) + return; + if (f->accessed) + acp_deaccess(f->dev_fd, f->chan); + if (f->chan) + acp_dassgn(f->dev_fd, f->chan); + imgact_acp_dev_close(f->dev_fd); + f->dev_fd = -1; + f->accessed = 0; + f->chan = 0; +} diff --git a/src/imgact/imgact_acp.h b/src/imgact/imgact_acp.h new file mode 100644 index 000000000..21b284065 --- /dev/null +++ b/src/imgact/imgact_acp.h @@ -0,0 +1,93 @@ +/* + * imgact_acp.h - IMGACT reads images through the executive Files-11 (ODS-2) ACP + * (vms-3e8e, rung of epic vms-208). + * + * On real OpenVMS an image is activated by ACCESSing its file on a channel + * $ASSIGNed to the volume and mapping its sections through the file's window + * ($QIO IO$_ACCESS + the image sections mapped through the retrieval-pointer + * window; VSI I/O User's Reference, "ACP-QIO Interface"). This module is the + * OVMX realisation of that model for the freestanding IMGACT.EXE: instead of + * open()/pread()/mmap() on a /vms POSIX path (the passthrough the Files-11 ACP + * pivot retires, docs/design-files11-acp-executive.md §4.6), it $ASSIGNs a + * file-class channel to the mounted ODS-2 volume, IO$_ACCESSes the image file + * by walking its directory chain, and reads its bytes with IO$_READVBLK -- all + * over /dev/vms. + * + * NO POSIX FALLBACK (CLAUDE.md Rule 9 / INV-6). Every entry point below fails + * honestly -- SS$_NOSUCHDEV when /dev/vms is unreachable or the boot volume is + * not ACP-mounted, SS$_NOSUCHFILE when the file is not on the volume -- and + * NEVER silently reads the image off a /vms POSIX tree instead. That silent + * fallback is exactly the LARP bug class the pivot killed. + * + * ATOMIC-FLIP-GROUP MEMBER (red-by-design). Boot does not yet ACP-mount + * SYS$DISK, so IMGACT's image reads through this path fail-honest at boot until + * the flip lands the executive-global $MOUNT of the system disk. Proven now + * against a real /dev/vms over an ACP-mounted fixture volume + * (tests/qemu/test_syssvc_imgact_acp.c). + * + * FREESTANDING + TESTABLE. IMGACT.EXE is -ffreestanding/-nostdlib and reaches + * /dev/vms via raw syscalls; the QEMU test is a hosted binary and reaches it + * via libc. The ONLY seam that differs is the three host primitives below -- + * so the test exercises the EXACT ACP read logic IMGACT runs, not a + * re-implementation of it. + */ + +#ifndef IMGACT_ACP_H +#define IMGACT_ACP_H + +#include + +/* -------------------------------------------------------------------------- + * Host primitives (the freestanding/hosted seam). IMGACT.EXE backs these with + * raw syscall6(); tests/qemu/test_syssvc_imgact_acp.c backs them with libc. + * -------------------------------------------------------------------------- */ + +/* Open /dev/vms for read/write; return a descriptor, or -1 on failure. */ +int imgact_acp_dev_open(void); +/* Close a descriptor returned by imgact_acp_dev_open(). */ +void imgact_acp_dev_close(int fd); +/* Issue one ioctl on `fd`; return 0 on success, a negative errno on failure. + * (The VMS SS$_ status is carried in the arg struct, not this return.) */ +long imgact_acp_dev_ioctl(int fd, unsigned long req, void *arg); + +/* -------------------------------------------------------------------------- + * An image file accessed over the Files-11 ACP. + * -------------------------------------------------------------------------- */ + +struct imgact_acp_file { + int dev_fd; /* /dev/vms descriptor owned by this handle */ + uint32_t chan; /* file-class channel $ASSIGNed to the volume */ + uint32_t valid; /* total valid bytes in the accessed file */ + int accessed; /* non-zero once IO$_ACCESS has the file open */ +}; + +/* + * $ASSIGN a file-class channel to `dev` (a mounted ODS-2 unit, e.g. "DKA0:") + * and IO$_ACCESS the file at `path`. `path` is '/'-separated; a leading "/vms" + * (the retired POSIX mount point) is stripped, each non-final component names + * an ODS-2 directory (walked as "NAME.DIR", DID chaining to each resolved + * sub-directory FID -- the VMS model), and the final component is the file + * "NAME.TYPE" (highest version). On success *f is left with the file accessed + * for read and ready for imgact_acp_pread(). + * + * Returns a VMS status (bit 0 set == success). Fail-honest: SS$_NOSUCHDEV + * (no /dev/vms, or the volume is not ACP-mounted), SS$_NOSUCHFILE (a path + * component is not on the volume). Never a POSIX fallback. + */ +uint32_t imgact_acp_open(struct imgact_acp_file *f, const char *dev, + const char *path); + +/* + * Read up to `n` bytes at byte offset `off` from the accessed file via + * IO$_READVBLK (the byte offset is resolved to {VBN, in-block offset} through + * the file's window). Returns the byte count transferred (>= 0; short, or 0, + * at end-of-file), or a negative value on error. Modelled on pread(2) so the + * activator's existing exact-count read checks keep working. + */ +long imgact_acp_pread(struct imgact_acp_file *f, void *buf, + unsigned long n, long off); + +/* IO$_DEACCESS the file, $DASSGN the channel, and close the /dev/vms fd. */ +void imgact_acp_close(struct imgact_acp_file *f); + +#endif /* IMGACT_ACP_H */ diff --git a/tests/qemu/CMakeLists.txt b/tests/qemu/CMakeLists.txt index c6ae78d25..e7e278ff5 100644 --- a/tests/qemu/CMakeLists.txt +++ b/tests/qemu/CMakeLists.txt @@ -98,6 +98,21 @@ foreach(_src ${QEMU_SYSSVC_SOURCES}) qemu_syssvc_add_test(${_name} ${_src}) endforeach() +# test_syssvc_imgact_acp (vms-3e8e) drives the EXACT freestanding ACP reader +# IMGACT.EXE runs (src/imgact/imgact_acp.c) against a real /dev/vms: the generic +# function above already registered the target off the glob, so augment it with +# that one extra source and the imgact/kernel/qemu headers it needs +# (imgact_acp.h, vms_ioctl.h + vms_acp.h, imgact_acp_fixture_elf.h). The test +# provides the three host primitives imgact_acp.c calls on libc. +if(TARGET test_syssvc_imgact_acp) + target_sources(test_syssvc_imgact_acp PRIVATE + ${CMAKE_SOURCE_DIR}/src/imgact/imgact_acp.c) + target_include_directories(test_syssvc_imgact_acp PRIVATE + ${CMAKE_SOURCE_DIR}/src/imgact + ${CMAKE_SOURCE_DIR}/src/kernel + ${CMAKE_CURRENT_SOURCE_DIR}) +endif() + # --------------------------------------------------------------------------- # DEVICE-CORPUS PROGRAMS UNDER A REAL /dev/vms (vms-08c). # diff --git a/tests/qemu/Dockerfile b/tests/qemu/Dockerfile index 73092f34c..805393d55 100644 --- a/tests/qemu/Dockerfile +++ b/tests/qemu/Dockerfile @@ -461,6 +461,22 @@ RUN gcc -O2 -Wall -I/src/vmsfs/include -o /src/tests/qemu/mkimage_ods2_search \ /src/vmsfs/ods2/ods2_bdev.c /src/vmsfs/ods2/ods2_block_posix.c && \ /src/tests/qemu/mkimage_ods2_search /ods2_search.img 1 +# GENERATED ODS-2 volume carrying a real ELF image [IMGACT]TESTIMG.EXE for the +# IMGACT-over-ACP proof (vms-3e8e, epic vms-208). mkimage_ods2_imgact.c lays the +# deterministic fixture ELF (tests/qemu/imgact_acp_fixture_elf.h) down with the +# SAME byte-genuine ODS-2 writer the other fixtures use. Staged as +# /ods2_imgact.img, which run_tests.sh copies onto DKA300: (vdd); +# test_syssvc_imgact_acp $MOUNTs it and reads the image header + PT_LOAD +# segments over IO$_ACCESS + IO$_READVBLK. +COPY tests/qemu/mkimage_ods2_imgact.c /src/tests/qemu/ +COPY tests/qemu/imgact_acp_fixture_elf.h /src/tests/qemu/ +RUN gcc -O2 -Wall -I/src/vmsfs/include -I/src/tests/qemu \ + -o /src/tests/qemu/mkimage_ods2_imgact \ + /src/tests/qemu/mkimage_ods2_imgact.c \ + /src/vmsfs/ods2/ods2_reader.c /src/vmsfs/ods2/ods2_writer.c \ + /src/vmsfs/ods2/ods2_bdev.c /src/vmsfs/ods2/ods2_block_posix.c && \ + /src/tests/qemu/mkimage_ods2_imgact /ods2_imgact.img 1 + # Locate the kernel image (may already exist as symlink) RUN KVER=$(cat /tmp/kver) && \ if [ ! -e /boot/vmlinuz ]; then \ diff --git a/tests/qemu/facility_defects.sh b/tests/qemu/facility_defects.sh index 2b0d1699c..c74e0518c 100755 --- a/tests/qemu/facility_defects.sh +++ b/tests/qemu/facility_defects.sh @@ -496,6 +496,7 @@ acp-mount-nonods2-accepted acp-access-window-vbn-offbyone acp-writevb-extend-alloc-offbyone acp-search-cursor-skips-versions +imgact-acp-valid-bytes-offbyone p0-map-not-recorded p1-map-not-recorded p0-unmap-clears-p1 @@ -4909,6 +4910,23 @@ EOF ;; esac;; + imgact-acp-valid-bytes-offbyone) + case "$_f" in + facility) echo "IMGACT image reads over the Files-11 (ODS-2) ACP -- the freestanding activator opens an image by IO\$_ACCESS (walking its directory chain) and reads its header + PT_LOAD segments by IO\$_READVBLK, decoding the accessed file's valid-byte count from its on-disk FH2 (efblk/ffbyte) so a read never over-reads past end-of-file, vms-3e8e, epic vms-208";; + targets) echo "imgact/imgact_acp.c";; + suites_red) echo "test_syssvc_imgact_acp";; + blind_suites) echo "";; + blind_why) echo "";; + isolation) echo "isolated";; + why) echo "acp_access_name() -- the IO\$_ACCESS helper that decodes the accessed image file's total valid-byte count from its FH2 -- computes (efblk - 1) * 512 + ffbyte + 1, one byte too many. That count is the file window's extent the reader clamps every IO\$_READVBLK to, so IMGACT would believe the image is one byte longer than it is. The over-count is HARMLESS to every in-bounds read -- the ELF header, the program-header table and each PT_LOAD segment all lie well inside the real file, so each imgact_acp_pread still clamps to their own lengths and returns byte-exact data -- which is exactly why only the assertion that reads the valid count BACK off the accessed file can tell the difference: the byte-exact header/phdr/segment/whole-image reads, the directory-walk open, and the fail-honest NOSUCHFILE/NOSUCHDEV checks all stay green, and only the on-disk-geometry assertion reddens. INV-6's shape: the accessed file's real size is a fact the executive holds on disk, not a value the reader may inflate.";; + require_fail) cat <<'EOF' +the accessed image's valid-byte count (1424) matches the on-disk FH2 +EOF + ;; + knock_on_fail) echo "";; + knock_on_why) echo "";; + esac;; + p0-map-not-recorded) case "$_f" in facility) echo "P0 program-region bookkeeping (VMS_IOCTL_P0_MAP/P0_UNMAP, vms-68f.i -- foundation increment of the Option A in-process image activation design, docs/design-in-process-activation.md Part II)";; @@ -6318,6 +6336,18 @@ apply_edit() { # nothing (the no-op selftest requires). sed -i 's|return version < c->prev_ver;|return version > c->prev_ver; /* NEGCTL acp-search-cursor-skips-versions */|' "$_file";; + imgact-acp-valid-bytes-offbyone) + # UNIQUE TEXT, no range anchor needed: `(efblk - 1u) * 512u` is the only + # valid-byte-count decode in imgact_acp.c (acp_access_name). Dropping the + # `- 1u` makes the accessed file's valid count one block too high, so the + # reader believes the image is 512 bytes longer than it is. Every in-bounds + # read (header, phdr table, each PT_LOAD, the whole 1424-byte image) still + # clamps to its own length and returns byte-exact data, so only the suite's + # on-disk-geometry assertion (valid == 1424) reddens. After substitution + # the `(efblk - 1u)` text is gone, so a second apply matches nothing (the + # no-op selftest requires). + sed -i 's|(efblk - 1u) \* 512u|efblk * 512u /* NEGCTL imgact-acp-valid-bytes-offbyone: valid overcounts by a block */|' "$_file";; + p0-map-not-recorded) # RANGE-ANCHORED to vms_ioctl_p0_map's own body: `proc->p0_base = # args.base;` immediately followed by `proc->p0_limit = args.limit;` @@ -7304,9 +7334,9 @@ cmd_selftest() { if ! cp -a "$_st_root/kernel" "$_st_root/kernel-core" \ "$_st_root/libvmssys" "$_st_root/libvms" \ "$_st_root/vmsdcl" "$_st_root/vmsrms" "$_st_root/vmslnm" \ - "$_st_root/vmsfs" "$_st_root/vmstcpip" \ + "$_st_root/vmsfs" "$_st_root/vmstcpip" "$_st_root/imgact" \ "$_st_tmp/tree/" 2>/dev/null; then - echo "FAIL: cannot copy $_st_root/{kernel,kernel-core,libvmssys,libvms,vmsdcl,vmsrms,vmslnm,vmsfs,vmstcpip} for the self-test" + echo "FAIL: cannot copy $_st_root/{kernel,kernel-core,libvmssys,libvms,vmsdcl,vmsrms,vmslnm,vmsfs,vmstcpip,imgact} for the self-test" rm -rf "$_st_tmp" return 2 fi diff --git a/tests/qemu/imgact_acp_fixture_elf.h b/tests/qemu/imgact_acp_fixture_elf.h new file mode 100644 index 000000000..ff97fef04 --- /dev/null +++ b/tests/qemu/imgact_acp_fixture_elf.h @@ -0,0 +1,116 @@ +/* + * imgact_acp_fixture_elf.h - a deterministic minimal ELF64 image, shared by the + * fixture builder (mkimage_ods2_imgact.c) and the proof (test_syssvc_imgact_acp.c). + * + * The builder lays these exact bytes down as [IMGACT]TESTIMG.EXE on a genuine + * ODS-2 volume; the test rebuilds the identical bytes in memory as the GOLDEN + * and asserts that what IMGACT's ACP reader (imgact_acp.c) reads back over + * IO$_ACCESS + IO$_READVBLK is byte-for-byte the same -- header AND every + * PT_LOAD segment. Because both sides call this one generator, the golden is + * the builder's own output, not a value the test invents. + * + * It is a WELL-FORMED ELF64 header + two PT_LOAD program headers with distinct, + * position-derived fill patterns; it is not meant to be executed (the item + * accepts "its bytes match the on-disk image" as the activation proof). The + * layout is fixed and self-describing so the test parses e_phoff/e_phnum and + * each segment's p_offset/p_filesz exactly as load_object() does. + */ + +#ifndef IMGACT_ACP_FIXTURE_ELF_H +#define IMGACT_ACP_FIXTURE_ELF_H + +#include +#include + +/* Fixed layout (bytes). Two PT_LOAD segments at block-aligned file offsets. */ +#define IMGACT_FIX_EHSZ 64u +#define IMGACT_FIX_PHOFF 64u +#define IMGACT_FIX_PHENT 56u +#define IMGACT_FIX_PHNUM 2u + +#define IMGACT_FIX_SEG0_OFF 512u +#define IMGACT_FIX_SEG0_SZ 300u +#define IMGACT_FIX_SEG0_VADDR 0x1000u + +#define IMGACT_FIX_SEG1_OFF 1024u +#define IMGACT_FIX_SEG1_SZ 400u +#define IMGACT_FIX_SEG1_VADDR 0x2000u + +#define IMGACT_FIX_TOTAL (IMGACT_FIX_SEG1_OFF + IMGACT_FIX_SEG1_SZ) /* 1424 */ + +/* Segment fill patterns (position-derived so a misread shows up as a mismatch). */ +static inline uint8_t imgact_fix_seg0_byte(unsigned i) { return (uint8_t)(0x11u ^ i); } +static inline uint8_t imgact_fix_seg1_byte(unsigned i) { return (uint8_t)(0x80u + i); } + +/* Little helpers so the header is endian-explicit regardless of host. */ +static inline void imgact_fix_put16(uint8_t *p, uint16_t v) +{ p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); } +static inline void imgact_fix_put32(uint8_t *p, uint32_t v) +{ p[0]=(uint8_t)v; p[1]=(uint8_t)(v>>8); p[2]=(uint8_t)(v>>16); p[3]=(uint8_t)(v>>24); } +static inline void imgact_fix_put64(uint8_t *p, uint64_t v) +{ imgact_fix_put32(p, (uint32_t)v); imgact_fix_put32(p + 4, (uint32_t)(v >> 32)); } + +/* One ELF64 program header (PT_LOAD) written at *p. */ +static inline void imgact_fix_phdr(uint8_t *p, uint32_t flags, uint64_t off, + uint64_t vaddr, uint64_t filesz) +{ + memset(p, 0, IMGACT_FIX_PHENT); + imgact_fix_put32(p + 0, 1); /* p_type = PT_LOAD */ + imgact_fix_put32(p + 4, flags); /* p_flags */ + imgact_fix_put64(p + 8, off); /* p_offset */ + imgact_fix_put64(p + 16, vaddr); /* p_vaddr */ + imgact_fix_put64(p + 24, vaddr); /* p_paddr */ + imgact_fix_put64(p + 32, filesz); /* p_filesz */ + imgact_fix_put64(p + 40, filesz); /* p_memsz */ + imgact_fix_put64(p + 48, 0x1000); /* p_align */ +} + +/* + * Build the fixture image into out[0..cap). Returns its total length + * (IMGACT_FIX_TOTAL) or 0 if cap is too small. + */ +static inline size_t imgact_acp_fixture_elf_build(uint8_t *out, size_t cap) +{ + unsigned i; + + if (cap < IMGACT_FIX_TOTAL) + return 0; + memset(out, 0, IMGACT_FIX_TOTAL); + + /* ELF64 header. */ + out[0] = 0x7f; out[1] = 'E'; out[2] = 'L'; out[3] = 'F'; + out[4] = 2; /* EI_CLASS = ELFCLASS64 */ + out[5] = 1; /* EI_DATA = ELFDATA2LSB */ + out[6] = 1; /* EI_VERSION */ + imgact_fix_put16(out + 16, 3); /* e_type = ET_DYN */ + imgact_fix_put16(out + 18, 62); /* e_machine = EM_X86_64 (nominal) */ + imgact_fix_put32(out + 20, 1); /* e_version */ + imgact_fix_put64(out + 24, IMGACT_FIX_SEG0_VADDR); /* e_entry */ + imgact_fix_put64(out + 32, IMGACT_FIX_PHOFF); /* e_phoff */ + imgact_fix_put64(out + 40, 0); /* e_shoff (none) */ + imgact_fix_put32(out + 48, 0); /* e_flags */ + imgact_fix_put16(out + 52, IMGACT_FIX_EHSZ); /* e_ehsize */ + imgact_fix_put16(out + 54, IMGACT_FIX_PHENT); /* e_phentsize */ + imgact_fix_put16(out + 56, IMGACT_FIX_PHNUM); /* e_phnum */ + imgact_fix_put16(out + 58, 0); /* e_shentsize */ + imgact_fix_put16(out + 60, 0); /* e_shnum */ + imgact_fix_put16(out + 62, 0); /* e_shstrndx */ + + /* Two PT_LOAD program headers. */ + imgact_fix_phdr(out + IMGACT_FIX_PHOFF, + 5 /* R+X */, IMGACT_FIX_SEG0_OFF, + IMGACT_FIX_SEG0_VADDR, IMGACT_FIX_SEG0_SZ); + imgact_fix_phdr(out + IMGACT_FIX_PHOFF + IMGACT_FIX_PHENT, + 6 /* R+W */, IMGACT_FIX_SEG1_OFF, + IMGACT_FIX_SEG1_VADDR, IMGACT_FIX_SEG1_SZ); + + /* Segment contents. */ + for (i = 0; i < IMGACT_FIX_SEG0_SZ; i++) + out[IMGACT_FIX_SEG0_OFF + i] = imgact_fix_seg0_byte(i); + for (i = 0; i < IMGACT_FIX_SEG1_SZ; i++) + out[IMGACT_FIX_SEG1_OFF + i] = imgact_fix_seg1_byte(i); + + return IMGACT_FIX_TOTAL; +} + +#endif /* IMGACT_ACP_FIXTURE_ELF_H */ diff --git a/tests/qemu/mkimage_ods2_imgact.c b/tests/qemu/mkimage_ods2_imgact.c new file mode 100644 index 000000000..e62cfd73b --- /dev/null +++ b/tests/qemu/mkimage_ods2_imgact.c @@ -0,0 +1,168 @@ +/* + * mkimage_ods2_imgact.c - Master a GENUINE ODS-2 (Files-11 L2, "DECFILE11B") + * test volume carrying a subdirectory [IMGACT] with a real ELF image + * TESTIMG.EXE, so tests/qemu/test_syssvc_imgact_acp.c can prove that IMGACT's + * executive Files-11 ACP reader (src/imgact/imgact_acp.c, vms-3e8e) activates + * an image by reading its header + PT_LOAD segments over IO$_ACCESS + + * IO$_READVBLK against a real /dev/vms -- byte-for-byte the on-disk image. + * + * The image bytes are the deterministic fixture ELF from + * tests/qemu/imgact_acp_fixture_elf.h, which the test rebuilds in memory as its + * golden, so the ground truth is the builder's own output (as mkimage_ods2_search + * does for the $SEARCH proof). Built with the SAME byte-genuine ODS-2 writer + * (src/vmsfs/ods2/ods2_writer.c) the other mkimage_ods2_* fixtures use -- it + * adds no new ODS-2 knowledge (CLAUDE.md Rule 8; provenance in vmsfs/ods2.h). + * + * [IMGACT]TESTIMG.EXE;1 (FID printed below; a well-formed ELF64 with + * two PT_LOAD segments, 1424 bytes) + * + * Usage: mkimage_ods2_imgact [size-in-MB] + */ + +#include +#include +#include +#include +#include +#include + +#include "vmsfs/ods2.h" +#include "imgact_acp_fixture_elf.h" + +#define DEFAULT_SIZE_MB 1 +#define MIN_BLOCKS 64 + +static int write_block(int fd, uint32_t lbn, const void *buf) +{ + off_t offset = (off_t)lbn * ODS2_BLOCK_SIZE; + if (lseek(fd, offset, SEEK_SET) != offset) + return -1; + if (write(fd, buf, ODS2_BLOCK_SIZE) != ODS2_BLOCK_SIZE) + return -1; + return 0; +} + +int main(int argc, char *argv[]) +{ + if (argc < 2 || argc > 3) { + fprintf(stderr, "Usage: mkimage_ods2_imgact [size-in-MB]\n"); + return 1; + } + const char *outpath = argv[1]; + uint64_t size_mb = (argc == 3) ? (uint64_t)strtoul(argv[2], NULL, 10) : DEFAULT_SIZE_MB; + uint64_t size_bytes = size_mb * 1024ULL * 1024ULL; + uint32_t total_blocks = (uint32_t)(size_bytes / ODS2_BLOCK_SIZE); + + if (total_blocks < MIN_BLOCKS) { + fprintf(stderr, "mkimage_ods2_imgact: volume too small (%u blocks, need %d)\n", + total_blocks, MIN_BLOCKS); + return 1; + } + + uint32_t maxfiles = total_blocks / 100; + if (maxfiles < 32) maxfiles = 32; + if (maxfiles > 65535) maxfiles = 65535; + if (maxfiles < ODS2_RESFILES) maxfiles = ODS2_RESFILES; + + size_t image_len = (size_t)total_blocks * ODS2_BLOCK_SIZE; + uint8_t *image = calloc(1, image_len); + if (!image) { + fprintf(stderr, "mkimage_ods2_imgact: cannot allocate %zu bytes\n", image_len); + return 1; + } + + ods2_format_params_t params; + params.total_blocks = total_blocks; + params.maxfiles = maxfiles; + params.volname = "OVMXIMGACT"; + + ods2_wvolume_t wvol; + ods2_status_t st = ods2_volume_format(image, image_len, ¶ms, &wvol); + if (st != ODS2_OK) { + fprintf(stderr, "mkimage_ods2_imgact: ods2_volume_format failed: %s\n", + ods2_strerror(st)); + free(image); + return 1; + } + + /* Subdirectory [IMGACT] under the MFD [000000]. */ + ods2_fid_t dir_fid; + st = ods2_wvolume_create_dir(&wvol, "IMGACT.DIR", 1, wvol.mfd_fid, &dir_fid); + if (st != ODS2_OK) { + fprintf(stderr, "mkimage_ods2_imgact: create_dir IMGACT.DIR failed: %s\n", + ods2_strerror(st)); + free(image); + return 1; + } + st = ods2_wvolume_dir_insert(&wvol, wvol.mfd_fid, "IMGACT.DIR", 1, dir_fid); + if (st != ODS2_OK) { + fprintf(stderr, "mkimage_ods2_imgact: dir_insert IMGACT.DIR failed: %s\n", + ods2_strerror(st)); + free(image); + return 1; + } + printf("mkimage_ods2_imgact: [000000]IMGACT.DIR;1 -> FID (%u,%u,%u)\n", + (unsigned)ods2_fid_number(&dir_fid), dir_fid.fid_seq, dir_fid.fid_rvn); + + /* The image file: the deterministic fixture ELF, laid down verbatim. */ + uint8_t elf[IMGACT_FIX_TOTAL]; + size_t elf_len = imgact_acp_fixture_elf_build(elf, sizeof(elf)); + if (elf_len != IMGACT_FIX_TOTAL) { + fprintf(stderr, "mkimage_ods2_imgact: fixture ELF build failed\n"); + free(image); + return 1; + } + + /* create_file_raw (RFM=FIXED, VERBATIM bytes), NOT create_file: a real .EXE + * image is stored unframed. create_file frames its input as RMS VAR text + * records (2-byte length words per line), which corrupts a binary -- the + * writer's own docs warn a boot master built on create_file "cannot store + * binaries". IMGACT then reads these bytes back byte-for-byte. */ + ods2_fid_t img_fid; + st = ods2_wvolume_create_file_raw(&wvol, "TESTIMG.EXE", 1, elf, elf_len, + dir_fid, &img_fid); + if (st != ODS2_OK) { + fprintf(stderr, "mkimage_ods2_imgact: create_file_raw TESTIMG.EXE failed: %s\n", + ods2_strerror(st)); + free(image); + return 1; + } + st = ods2_wvolume_dir_insert(&wvol, dir_fid, "TESTIMG.EXE", 1, img_fid); + if (st != ODS2_OK) { + fprintf(stderr, "mkimage_ods2_imgact: dir_insert TESTIMG.EXE failed: %s\n", + ods2_strerror(st)); + free(image); + return 1; + } + printf("mkimage_ods2_imgact: [IMGACT]TESTIMG.EXE;1 -> FID (%u,%u,%u), %zu bytes\n", + (unsigned)ods2_fid_number(&img_fid), img_fid.fid_seq, img_fid.fid_rvn, + elf_len); + + int fd = open(outpath, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + perror(outpath); + free(image); + return 1; + } + if (ftruncate(fd, (off_t)image_len) < 0) { + perror("ftruncate"); + close(fd); + free(image); + return 1; + } + for (uint32_t lbn = 0; lbn < wvol.next_free_lbn; lbn++) { + if (write_block(fd, lbn, image + (size_t)lbn * ODS2_BLOCK_SIZE) < 0) { + perror("write_block"); + close(fd); + free(image); + return 1; + } + } + + close(fd); + free(image); + + printf("mkimage_ods2_imgact: wrote %s (%u blocks, %u max files, genuine ODS-2/DECFILE11B)\n", + outpath, total_blocks, maxfiles); + return 0; +} diff --git a/tests/qemu/run_tests.sh b/tests/qemu/run_tests.sh index 124714e8a..366fafbf4 100644 --- a/tests/qemu/run_tests.sh +++ b/tests/qemu/run_tests.sh @@ -71,11 +71,12 @@ KCMD_SHARD="ovmx.shard=$SHARD_INDEX ovmx.shards=$SHARD_TOTAL" # disclosed, not silently assumed fixed -- see the comment there. ASSERT_TRANSCRIPT=$(mktemp) || { echo "run_tests.sh: mktemp failed" >&2; exit 2; } -# Two virtio disks (vms-3e8). The executive enumerates the node's virtio block -# devices into DK units (DKA0: from vda, DKA100: from vdb) at module init -- -# test_kmod_disk asserts that mapping against a real vms.ko, so the guest must -# actually HAVE two virtio disks. Cleaned up with the transcript below (ONE trap, -# all temp files -- see the trap note further down). +# Four virtio disks (vms-3e8 + vms-3e8e). The executive enumerates the node's +# virtio block devices into DK units (DKA0: from vda, DKA100: from vdb, DKA200: +# from vdc, DKA300: from vdd) at module init -- test_kmod_disk asserts the +# vda/vdb mapping against a real vms.ko, so the guest must actually HAVE the +# disks. Cleaned up with the transcript below (ONE trap, all temp files -- see +# the trap note further down). # # vda (DKA0:) -- a GENUINE real-VAX ODS-2 volume: the SYSTEM DISK. The # Files-11 ACP $MOUNT VALIDATES a real Files-11 structure (home @@ -94,6 +95,7 @@ ASSERT_TRANSCRIPT=$(mktemp) || { echo "run_tests.sh: mktemp failed" >&2; exit 2; OVMX_DISK0=$(mktemp) || { echo "run_tests.sh: mktemp failed" >&2; exit 2; } OVMX_DISK1=$(mktemp) || { echo "run_tests.sh: mktemp failed" >&2; exit 2; } OVMX_DISK2=$(mktemp) || { echo "run_tests.sh: mktemp failed" >&2; exit 2; } +OVMX_DISK3=$(mktemp) || { echo "run_tests.sh: mktemp failed" >&2; exit 2; } truncate -s 16M "$OVMX_DISK1" OVMX_ODS2_SRC=/ods2_real.img if [ -f "$OVMX_ODS2_SRC" ]; then @@ -117,7 +119,21 @@ else echo " the \$SEARCH test (vms-a0b) needs a multi-version ODS-2 volume on DKA200: (vdc)" >&2 exit 2 fi -trap 'rm -f "$ASSERT_TRANSCRIPT" "$OVMX_DISK0" "$OVMX_DISK1" "$OVMX_DISK2"' EXIT +# vdd (DKA300:) -- a GENERATED genuine ODS-2 volume carrying [IMGACT]TESTIMG.EXE +# (a real ELF image) from /ods2_imgact.img == +# tests/qemu/mkimage_ods2_imgact.c. test_syssvc_imgact_acp +# (vms-3e8e) $MOUNTs it and has IMGACT's freestanding ACP +# reader activate the image -- header + PT_LOAD via +# IO$_ACCESS + IO$_READVBLK, byte-exact vs the on-disk bytes. +OVMX_ODS2_IMGACT_SRC=/ods2_imgact.img +if [ -f "$OVMX_ODS2_IMGACT_SRC" ]; then + cp "$OVMX_ODS2_IMGACT_SRC" "$OVMX_DISK3" +else + echo "run_tests.sh: FATAL: ODS-2 imgact fixture $OVMX_ODS2_IMGACT_SRC missing --" >&2 + echo " the IMGACT-over-ACP test (vms-3e8e) needs an ODS-2 image volume on DKA300: (vdd)" >&2 + exit 2 +fi +trap 'rm -f "$ASSERT_TRANSCRIPT" "$OVMX_DISK0" "$OVMX_DISK1" "$OVMX_DISK2" "$OVMX_DISK3"' EXIT # One virtio-net NIC (vms-9d2). Exactly as the two virtio disks above give the # executive real block devices to enumerate into DK units, this gives it a real @@ -187,6 +203,8 @@ OUTPUT=$(timeout "$TIMEOUT" $QEMU \ -device virtio-blk-pci,drive=ovmxdisk1 \ -drive if=none,id=ovmxdisk2,file="$OVMX_DISK2",format=raw \ -device virtio-blk-pci,drive=ovmxdisk2 \ + -drive if=none,id=ovmxdisk3,file="$OVMX_DISK3",format=raw \ + -device virtio-blk-pci,drive=ovmxdisk3 \ 2>&1) || QEMU_RC=$? # Splice the assertion transcript (ttyS1, if this arch has one) back into diff --git a/tests/qemu/test_kmod_disk.c b/tests/qemu/test_kmod_disk.c index 9bd5fca69..760bec210 100644 --- a/tests/qemu/test_kmod_disk.c +++ b/tests/qemu/test_kmod_disk.c @@ -66,8 +66,8 @@ int main(void) char backing[16]; uint32_t maj = 0, min = 0, status; uint32_t vda_maj = 0, vda_min = 0, vdb_maj = 0, vdb_min = 0; - uint32_t vdc_maj = 0, vdc_min = 0; - int have_vda, have_vdb, have_vdc; + uint32_t vdc_maj = 0, vdc_min = 0, vdd_maj = 0, vdd_min = 0; + int have_vda, have_vdb, have_vdc, have_vdd; printf("=== test_kmod_disk: the executive names the machine's disks ===\n"); @@ -87,9 +87,11 @@ int main(void) have_vda = (stat_devt("/dev/vda", &vda_maj, &vda_min) == 0); have_vdb = (stat_devt("/dev/vdb", &vdb_maj, &vdb_min) == 0); have_vdc = (stat_devt("/dev/vdc", &vdc_maj, &vdc_min) == 0); + have_vdd = (stat_devt("/dev/vdd", &vdd_maj, &vdd_min) == 0); CHECK(have_vda, "/dev/vda is present (first virtio disk attached to the guest)"); CHECK(have_vdb, "/dev/vdb is present (second virtio disk attached to the guest)"); CHECK(have_vdc, "/dev/vdc is present (third virtio disk attached to the guest)"); + CHECK(have_vdd, "/dev/vdd is present (fourth virtio disk attached to the guest)"); /* -------------------------------------------------------------- * 1. DKA0: exists in the executive's table -- nothing in this @@ -134,14 +136,29 @@ int main(void) "DKA200: backing dev_t matches /dev/vdc as userspace stat()s it"); /* -------------------------------------------------------------- - * 4. Negative controls -- a resolver that always succeeded would be - * indistinguishable from one that works. + * 4. DKA300: is the fourth disk, vdd (the generated ODS-2 volume with + * [IMGACT]TESTIMG.EXE the IMGACT-over-ACP test mounts; vms-3e8e added + * it to run_tests.sh). * -------------------------------------------------------------- */ - /* Three disks are attached, so there is no fourth unit. */ memset(backing, 0, sizeof(backing)); + maj = min = 0; status = vms_kif_disk_resolve("DKA300:", backing, sizeof(backing), &maj, &min); + CHECK(status == SS_NORMAL, + "DKA300: exists in the executive's table (the fourth disk)"); + CHECK(strcmp(backing, "vdd") == 0, + "DKA300: backing device is vdd (the executive's enumeration)"); + CHECK(have_vdd && maj == vdd_maj && min == vdd_min, + "DKA300: backing dev_t matches /dev/vdd as userspace stat()s it"); + + /* -------------------------------------------------------------- + * 5. Negative controls -- a resolver that always succeeded would be + * indistinguishable from one that works. + * -------------------------------------------------------------- */ + /* Four disks are attached, so there is no fifth unit. */ + memset(backing, 0, sizeof(backing)); + status = vms_kif_disk_resolve("DKA400:", backing, sizeof(backing), &maj, &min); CHECK(status == SS_NOSUCHDEV, - "a disk unit that does not exist reports SS$_NOSUCHDEV (no fourth disk attached)"); + "a disk unit that does not exist reports SS$_NOSUCHDEV (no fifth disk attached)"); /* OPA0: exists, but it is a TERMINAL -- it has no backing block device. */ memset(backing, 0, sizeof(backing)); diff --git a/tests/qemu/test_syssvc_imgact_acp.c b/tests/qemu/test_syssvc_imgact_acp.c new file mode 100644 index 000000000..4f6dff10c --- /dev/null +++ b/tests/qemu/test_syssvc_imgact_acp.c @@ -0,0 +1,235 @@ +/* + * test_syssvc_imgact_acp.c - IMGACT activates an image by reading its ELF header + * and PT_LOAD segments over the executive Files-11 (ODS-2) ACP -- IO$_ACCESS + + * IO$_READVBLK on a channel $ASSIGNed to a mounted volume, NOT open()/pread() + * on a /vms POSIX path (vms-3e8e, rung of epic vms-208), proven against a real + * /dev/vms. + * + * This drives the EXACT freestanding reader IMGACT.EXE runs: src/imgact/ + * imgact_acp.c is compiled into this test unchanged (CMakeLists), and the only + * seam that differs -- the three host primitives imgact_acp_dev_{open,close, + * ioctl} -- is provided below on libc instead of raw syscalls. So a pass here + * is a pass of the activator's real ACP path, not a re-implementation. + * + * WHAT THIS PROVES, against a real /dev/vms over the generated ODS-2 fixture the + * harness seeds on DKA300: (vdd, from tests/qemu/mkimage_ods2_imgact.c): + * + * 1. OPEN BY FILESPEC + DIRECTORY WALK. imgact_acp_open("DKA300:", + * "/IMGACT/TESTIMG.EXE") $ASSIGNs a file-class channel, walks [IMGACT] as + * an ODS-2 directory (IO$_ACCESS "IMGACT.DIR", DID-chaining to its FID), + * then IO$_ACCESSes TESTIMG.EXE -- resolving the real image file and its + * valid-byte count (1424) off the on-disk FH2. + * + * 2. IMAGE HEADER via IO$_READVBLK. The ELF64 header read at byte offset 0 is + * well-formed (magic, ELFCLASS64) and carries the fixture's two program + * headers -- byte-for-byte the on-disk header. + * + * 3. PT_LOAD SEGMENTS via IO$_READVBLK. Each PT_LOAD's p_filesz bytes, read at + * its p_offset exactly as load_object() maps them, are byte-identical to + * the committed fixture (the builder's own output, imgact_acp_fixture_elf.h). + * + * 4. WHOLE IMAGE BYTE-EXACT. The entire file read back over the window equals + * the golden image -- the read-then-map first cut's core guarantee. + * + * 5. FAIL-HONEST, NO POSIX FALLBACK (INV-6). A name not on the volume is + * SS$_NOSUCHFILE; a unit that is not an ACP-mounted volume is + * SS$_NOSUCHDEV -- never a silent read off a /vms POSIX tree. + * + * NO /dev/vms -> honest SKIP (77): the ACP, the volume mount, the file window + * and the transfer are all executive-resident, so with no /dev/vms there is + * nothing to assert (the contract every test_syssvc_* suite is held to). + * + * GROUND TRUTH. The image bytes are the deterministic fixture ELF the builder + * laid down; this test rebuilds the identical bytes in memory as the golden, so + * every assertion below is against the fixture's own output, not values this + * test invents. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ssdef.h" +#include "vms_kif.h" +#include "vms/pcb.h" +#include "imgact_acp.h" +#include "imgact_acp_fixture_elf.h" + +#define EXIT_SKIP 77 + +/* DKA300: (vdd) carries the generated ODS-2 fixture with [IMGACT]TESTIMG.EXE. */ +#define ODS2_UNIT "DKA300:" +#define IMG_PATH "/IMGACT/TESTIMG.EXE" + +/* -------------------------------------------------------------------------- + * Host primitives (the freestanding/hosted seam). IMGACT.EXE backs these with + * raw syscall6(); here they are libc, so imgact_acp.c runs unchanged. + * -------------------------------------------------------------------------- */ +int imgact_acp_dev_open(void) +{ + return open("/dev/vms", O_RDWR); +} +void imgact_acp_dev_close(int fd) +{ + if (fd >= 0) + close(fd); +} +long imgact_acp_dev_ioctl(int fd, unsigned long req, void *arg) +{ + return ioctl(fd, req, arg) < 0 ? -1 : 0; +} + +static int pass = 0; +static int fail = 0; + +static void check(int cond, const char *name) +{ + if (cond) { printf(" PASS: %s\n", name); pass++; } + else { printf(" FAIL: %s\n", name); fail++; } +} + +static int executive_present(void) +{ + int fd = vms_kif_open(); + if (fd < 0) + return 0; + vms_kif_close(); + return 1; +} + +int main(void) +{ + struct imgact_acp_file f; + uint32_t st; + uint8_t golden[IMGACT_FIX_TOTAL]; + uint8_t buf[IMGACT_FIX_TOTAL]; + Elf64_Ehdr eh; + Elf64_Phdr ph[8]; + long got; + int i; + + setvbuf(stdout, NULL, _IOLBF, 0); + + printf("=== test_syssvc_imgact_acp: IMGACT reads an image (header + PT_LOAD) over " + "the Files-11 ACP IO$_ACCESS+IO$_READVBLK (vms-3e8e, epic vms-208) ===\n"); + + if (!vms_pcb_init(0xFFFFFFFFFFFFFFFFULL)) { + printf(" FAIL: vms_pcb_init() failed\n"); + return 1; + } + + if (!executive_present()) { + printf("=== test_syssvc_imgact_acp: 0 passed, 0 failed (SKIPPED: no /dev/vms -- " + "the ACP, the mount, the file window and the transfer are executive-resident) ===\n"); + return EXIT_SKIP; + } + + /* The committed golden: the builder's own fixture image, rebuilt here. */ + if (imgact_acp_fixture_elf_build(golden, sizeof(golden)) != IMGACT_FIX_TOTAL) { + printf(" FAIL: fixture golden build\n"); + return 1; + } + + /* Precondition: the boot-time $MOUNT PID 1 will do for SYS$DISK. IMGACT + * itself never mounts -- it only $ASSIGNs -- so the mount is the harness's + * (as it is the executive's job in the real boot the atomic flip lands). */ + st = vms_kif_acp_mount(ODS2_UNIT); + check($VMS_STATUS_SUCCESS(st), + "$MOUNT of the generated ODS-2 " ODS2_UNIT " (precondition)"); + if (!$VMS_STATUS_SUCCESS(st)) { + printf("=== test_syssvc_imgact_acp: %d passed, %d failed ===\n", pass, fail); + return 1; + } + + /* --- (1) open by filespec: $ASSIGN + directory walk + IO$_ACCESS ------ */ + st = imgact_acp_open(&f, ODS2_UNIT, IMG_PATH); + check($VMS_STATUS_SUCCESS(st), + "imgact_acp_open walks [IMGACT] and IO$_ACCESSes TESTIMG.EXE over the ACP"); + /* negctl: imgact-acp-valid-bytes-offbyone */ + check($VMS_STATUS_SUCCESS(st) && f.valid == IMGACT_FIX_TOTAL, + "the accessed image's valid-byte count (1424) matches the on-disk FH2"); + if (!$VMS_STATUS_SUCCESS(st)) { + printf("=== test_syssvc_imgact_acp: %d passed, %d failed ===\n", pass, fail); + return 1; + } + + /* --- (2) image header via IO$_READVBLK -------------------------------- */ + got = imgact_acp_pread(&f, &eh, sizeof(eh), 0); + if (eh.e_phnum > 8) /* guard: a garbage header must not overflow ph[] */ + eh.e_phnum = 8; + check(got == (long)sizeof(eh) && + eh.e_ident[0] == 0x7f && eh.e_ident[1] == 'E' && + eh.e_ident[2] == 'L' && eh.e_ident[3] == 'F' && + eh.e_ident[EI_CLASS] == ELFCLASS64, + "IO$_READVBLK reads a well-formed ELF64 header at offset 0"); + check(got == (long)sizeof(eh) && eh.e_phnum == IMGACT_FIX_PHNUM && + eh.e_phoff == IMGACT_FIX_PHOFF, + "the header carries the fixture's two program headers (e_phnum=2, e_phoff=64)"); + check(got == (long)sizeof(eh) && memcmp(&eh, golden, sizeof(eh)) == 0, + "the ACP-read ELF header is BYTE-EXACT vs the on-disk image"); + + /* --- (3) each PT_LOAD segment via IO$_READVBLK ------------------------ */ + got = imgact_acp_pread(&f, ph, + (unsigned long)eh.e_phnum * sizeof(Elf64_Phdr), + (long)eh.e_phoff); + check(got == (long)((long)eh.e_phnum * (long)sizeof(Elf64_Phdr)), + "IO$_READVBLK reads the program-header table"); + check(memcmp(ph, golden + eh.e_phoff, + (size_t)eh.e_phnum * sizeof(Elf64_Phdr)) == 0, + "the ACP-read program-header table is BYTE-EXACT vs the on-disk image"); + + { + int loads = 0, seg_ok = 1; + for (i = 0; i < (int)eh.e_phnum; i++) { + uint8_t seg[IMGACT_FIX_TOTAL]; + if (ph[i].p_type != PT_LOAD || ph[i].p_filesz == 0) + continue; + if (ph[i].p_filesz > IMGACT_FIX_TOTAL || + ph[i].p_offset > IMGACT_FIX_TOTAL) { /* garbage guard */ + seg_ok = 0; + continue; + } + loads++; + got = imgact_acp_pread(&f, seg, ph[i].p_filesz, + (long)ph[i].p_offset); + if (got != (long)ph[i].p_filesz || + memcmp(seg, golden + ph[i].p_offset, + (size_t)ph[i].p_filesz) != 0) + seg_ok = 0; + } + check(loads == 2, "the image has two PT_LOAD segments (as load_object walks them)"); + check(seg_ok, + "every PT_LOAD's p_filesz bytes, read at p_offset via IO$_READVBLK, " + "are BYTE-EXACT vs the on-disk image"); + } + + /* --- (4) whole image byte-exact over the window ----------------------- */ + got = imgact_acp_pread(&f, buf, IMGACT_FIX_TOTAL, 0); + check(got == (long)IMGACT_FIX_TOTAL && + memcmp(buf, golden, IMGACT_FIX_TOTAL) == 0, + "the whole image read back over the ACP window is BYTE-EXACT vs the golden"); + + imgact_acp_close(&f); + + /* --- (5) fail-honest: no POSIX fallback ------------------------------- */ + { + struct imgact_acp_file nf; + st = imgact_acp_open(&nf, ODS2_UNIT, "/IMGACT/NOSUCH.EXE"); + check(st == SS$_NOSUCHFILE, + "an image name not on the volume is SS$_NOSUCHFILE (no POSIX fallback, INV-6)"); + imgact_acp_close(&nf); + + st = imgact_acp_open(&nf, "DKA999:", IMG_PATH); + check(st == SS$_NOSUCHDEV, + "a unit that is not an ACP-mounted volume is SS$_NOSUCHDEV (no POSIX fallback, INV-6)"); + imgact_acp_close(&nf); + } + + printf("=== test_syssvc_imgact_acp: %d passed, %d failed ===\n", pass, fail); + return fail == 0 ? 0 : 1; +} From 78b1eaf70709d8157f78a3a62746806b8a9ab090 Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 17 Aug 2026 04:17:35 +0000 Subject: [PATCH 003/106] =?UTF-8?q?vms-bc7:=20RMS=20reaches=20files=20via?= =?UTF-8?q?=20channel=20+=20$QIO=20to=20the=20ACP;=20FAB.=5Flinux=5Ffd=20r?= =?UTF-8?q?emoved=20(ATOMIC-FLIP-GROUP=20=E2=80=94=20red-by-design)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RMS no longer does positioned POSIX I/O on a per-process fd. FAB._linux_fd is retired; RMS reaches file data through the Files-11 ODS-2 ACP (epic vms-208): $OPEN -> $ASSIGN SYS$DISK + IO$_ACCESS (name->FID, VBN->LBN window) $CREATE -> IO$_CREATE(+IO$M_ACCESS) ($ERASE -> IO$_DELETE) $CLOSE -> IO$_DEACCESS + $DASSGN ($EXTEND -> IO$_MODIFY) $GET/$PUT record I/O -> IO$_READVBLK / IO$_WRITEVBLK at {VBN, byte-offset} resolve_filename resolves via the ACP, NOT vmsfs_to_linux_path. The block-I/O SUBSTRATE swap: a new rms_io.c re-homes the POSIX-fd cursor vocabulary (lseek/read/write/read_exact/write_exact/ftruncate/fsync) the seq/rel/idx record engines depend on onto {VBN,offset,length} READVBLK/WRITEVBLK on the channel window. The record logic (RFM framing, cursor arithmetic, key compares) is UNCHANGED -- only the fd+pread beneath it becomes channel+$QIO. Two backends behind one interface: __linux__ = the ACP (product runtime); otherwise = POSIX (the netbsd-vax standalone cross, until VAX's own ACP re-target vms-d5d). No silent POSIX fallback on Linux -- an absent /dev/vms is the real RMS/SS$ error (INV-6). Scope: SEQUENTIAL (VAR/STMLF/FIX) proven byte-exact end-to-end. RELATIVE rides the same substrate (cell pre-alloc via IO$_MODIFY). INDEXED is fail-honest DEFERRED on the ACP (RMS$_ORG): its data fork rides the substrate, but the ODS-2 prologue/bucket index has no ACP home yet -- a separate rung. Record attributes (RFM/RAT/MRS) are supplied on the FAB; FAT persistence via an extended IO$_CREATE ATR is deferred (the sidecar is retired on Linux). PROVEN on a real /dev/vms (tests/qemu/test_syssvc_rms_acp.c, QEMU kernel-executive harness): RMS-over-ACP 38 passed, 0 failed -- $CREATE+$PUT lands records via WRITEVBLK, $CLOSE + re-$OPEN + $GET reads them back byte/record-exact via READVBLK for VAR, STMLF and FIX; $EXTEND grows allocation; $ERASE deletes (subsequent $OPEN is RMS$_FNF). ATOMIC-FLIP-GROUP, red-by-design, DO NOT MERGE STANDALONE: existing RMS/DCL/MMK suites that hit SYS$DISK now fail-honest (no ACP-mounted SYS$DISK at boot yet -- that mount co-lands with the flip). Expected-red: vmsrms_unit, vmsrms_idx_close_flush, parts_rms_indexed_functional, toolchain-mmk-parse, toolchain-mmk-component-plan (host ctest); test_syssvc_rms_scratch_create, test_syssvc_mmk_build, test_syssvc_mmk_drive (QEMU). Stacks on #644 (work/vms-5303-acp-create). Rebase onto main after #644 merges. Cascade: vms_kif.h OVMX-UNWIRED annotations for acp_access/deaccess/readvb/ writevb/fileop deleted (RMS is now their product caller -- census gate green); rms_core/rms_record OVMX service-register annotations updated to PARTIAL (register gate green); mk_vmsrms_shr.sh native-link enumeration adds rms_io + libvmssys include (acp symbols already in libvmssys_shr.vec; strtok_r already in DECC$SHR); genuine negctl anchor rms-put-wrong-vbn added (coverage PASS, FLOOR-NO-BUMP). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/apps/parts/parts_db.c | 2 +- src/libvmssys/vms_kif.h | 27 +- src/vmslink/mk_vmsrms_shr.sh | 12 +- src/vmsrms/CMakeLists.txt | 10 + src/vmsrms/include/rms/fab.h | 17 +- src/vmsrms/rms_core.c | 618 ++++++++++++++++++-- src/vmsrms/rms_idx.c | 111 ++-- src/vmsrms/rms_io.c | 337 +++++++++++ src/vmsrms/rms_io.h | 112 ++++ src/vmsrms/rms_record.c | 45 +- src/vmsrms/rms_rel.c | 55 +- src/vmsrms/rms_seq.c | 77 +-- tests/qemu/facility_defects.sh | 29 + tests/qemu/test_syssvc_rms_acp.c | 258 ++++++++ tests/qemu/test_syssvc_rms_scratch_create.c | 2 +- tests/vmsrms/test_vmsrms.c | 4 +- 16 files changed, 1493 insertions(+), 223 deletions(-) create mode 100644 src/vmsrms/rms_io.c create mode 100644 src/vmsrms/rms_io.h create mode 100644 tests/qemu/test_syssvc_rms_acp.c diff --git a/src/apps/parts/parts_db.c b/src/apps/parts/parts_db.c index da157535c..da71e3f4c 100644 --- a/src/apps/parts/parts_db.c +++ b/src/apps/parts/parts_db.c @@ -70,7 +70,7 @@ static void init_fab(struct FAB *fab, struct XABKEY *xab, fab->fab$l_fna = (char *)filespec; fab->fab$b_fns = (uint8_t)strlen(filespec); fab->fab$l_xab = xab; - fab->_linux_fd = -1; + fab->_rms_file = 0; /* vms-bc7: RMS handle (was _linux_fd) */ } static void init_rab(struct RAB *rab, struct FAB *fab) diff --git a/src/libvmssys/vms_kif.h b/src/libvmssys/vms_kif.h index 59f683fe0..47791cce5 100644 --- a/src/libvmssys/vms_kif.h +++ b/src/libvmssys/vms_kif.h @@ -791,12 +791,11 @@ uint32_t vms_kif_acp_assign(const char *devnam, uint32_t *exec_chan); * SS$_NOSUCHDEV if /dev/vms is absent). See src/kernel/vms_acp.h for the FIB/ATR * interface. * - * OVMX-UNWIRED: vms_kif_acp_access (vms-204) -- no product caller yet: RMS - * $OPEN/$CLOSE is the path that will emit these, and that is the next rung - * (RMS-over-$QIO) of epic vms-208. Exercised today only by - * tests/qemu/test_syssvc_acp_access.c against a real /dev/vms. - * OVMX-UNWIRED: vms_kif_acp_deaccess (vms-204) -- the IO$_DEACCESS counterpart - * of vms_kif_acp_access above, same reason, same test. */ + * WIRED (vms-bc7, epic vms-208): RMS $OPEN/$CREATE emit vms_kif_acp_access to + * open a file on the SYS$DISK channel (src/vmsrms/rms_core.c rms_acp_open_file, + * and rms_acp_resolve_did walking directory components), and $CLOSE emits + * vms_kif_acp_deaccess (rms_impl_close / rms_acp_close_handle). Also exercised + * by tests/qemu/test_syssvc_acp_access.c and test_syssvc_rms_acp.c. */ uint32_t vms_kif_acp_access(struct vms_acp_access_args *args); uint32_t vms_kif_acp_deaccess(uint32_t chan); /* @@ -805,9 +804,11 @@ uint32_t vms_kif_acp_deaccess(uint32_t chan); * allocation + FH2 grow). args carries the transfer control fields + a user data * `buffer` pointer; see src/kernel/vms_acp.h. Fail-honest (SS$_NOSUCHDEV if * /dev/vms is absent). Exercised only by tests/qemu/test_syssvc_acp_rw.c against - * a real /dev/vms until the RMS-over-$QIO rung wires $GET/$PUT to them. - * OVMX-UNWIRED: vms_kif_acp_readvb (vms-c60) -- no product caller yet; RMS $GET reaches it on the RMS-over-$QIO rung - * OVMX-UNWIRED: vms_kif_acp_writevb (vms-c60) -- no product caller yet; RMS $PUT reaches it on the RMS-over-$QIO rung + * a real /dev/vms. + * WIRED (vms-bc7, epic vms-208): RMS record I/O rides these -- $GET is + * vms_kif_acp_readvb and $PUT is vms_kif_acp_writevb, through the block-I/O + * substrate src/vmsrms/rms_io.c (rms_io_read / rms_io_write) under the seq/rel + * record engines; also exercised by test_syssvc_acp_rw.c / test_syssvc_rms_acp.c. */ uint32_t vms_kif_acp_readvb(struct vms_acp_rw_args *args); uint32_t vms_kif_acp_writevb(struct vms_acp_rw_args *args); @@ -829,9 +830,11 @@ uint32_t vms_kif_acp_acpcontrol(struct vms_acp_acpcontrol_args *args); * and blocks), modify a file (extend/truncate/write attributes). func-dispatched * on args->func (VMS_ACP_FOP_*). See src/kernel/vms_acp.h for the FIB/ATR * interface and the ioctl-mapping decision. Returns SS$_NOSUCHDEV if /dev/vms - * is absent. OVMX-UNWIRED: vms_kif_acp_fileop (vms-5303) -- no product caller - * yet (RMS $CREATE/$ERASE/$EXTEND wire to it in a later rung); exercised by - * tests/qemu/test_syssvc_acp_create.c against real /dev/vms. */ + * is absent. WIRED (vms-bc7, epic vms-208): RMS $CREATE emits IO$_CREATE, + * $ERASE emits IO$_DELETE, and $EXTEND / truncate emit IO$_MODIFY through this + * umbrella (src/vmsrms/rms_core.c rms_impl_create/_erase/_extend/_close and + * rms_io.c rms_io_ftruncate); also exercised by tests/qemu/test_syssvc_acp_create.c + * and test_syssvc_rms_acp.c against real /dev/vms. */ uint32_t vms_kif_acp_fileop(struct vms_acp_fileop_args *args); #endif /* _VMS_KIF_H */ diff --git a/src/vmslink/mk_vmsrms_shr.sh b/src/vmslink/mk_vmsrms_shr.sh index 35ebff1a4..f0dd8ca92 100755 --- a/src/vmslink/mk_vmsrms_shr.sh +++ b/src/vmslink/mk_vmsrms_shr.sh @@ -68,6 +68,11 @@ HERE=$(cd "$(dirname "$0")" && pwd) # src/vmslink SRC=${6:-$(cd "$HERE/../vmsrms" && pwd)} # src/vmsrms LIBVMS_INC=${7:-$(cd "$HERE/../libvms/include" && pwd)} # ovmx_layout.h/ssdef.h/rmsdef.h VMSFS_INC=${8:-$(cd "$HERE/../vmsfs/include" && pwd)} # vmsfs/filespec.h, version.h +# vms-bc7: rms_io.c / rms_core.c include vms_kif.h (the /dev/vms ACP wrappers); +# vms_kif.h pulls ../kernel/vms_ioctl.h -> vms_acp.h. The vms_kif_acp_* symbols +# they import are producers in libvmssys_shr.vec (already exported), resolved by +# --use at native link -- this only needs the header on the compile path. +LIBVMSSYS_INC=${9:-$(cd "$HERE/../libvmssys" && pwd)} # vms_kif.h CC=${CC:-gcc} GSMATCH=${GSMATCH:-LEQUAL,1,0} @@ -80,14 +85,15 @@ WORK=${WORK:-/tmp/mk-vmsrms-shr} mkdir -p "$WORK" CFLAGS="${CFLAGS:--fPIC -O2 -ffreestanding -fno-builtin -fno-stack-protector -mno-outline-atomics -U_FORTIFY_SOURCE}" -INCS="-I$SRC/include -I$LIBVMS_INC -I$VMSFS_INC" +INCS="-I$SRC/include -I$LIBVMS_INC -I$VMSFS_INC -I$LIBVMSSYS_INC" echo "mk_vmsrms_shr: LINK.EXE=$LINK_EXE CC=$CC GSMATCH=$GSMATCH" echo "mk_vmsrms_shr: src=$SRC" echo "mk_vmsrms_shr: --use $DECC_SHR $VMS_SHR $FS_SHR" -# The 8 translation units of the vmsrms library (== src/vmsrms/CMakeLists.txt). -LIST="rms_core rms_seq rms_rel rms_idx rms_record rms_parse rms_search rms_util" +# The translation units of the vmsrms library (== src/vmsrms/CMakeLists.txt). +# vms-bc7 added rms_io.c (the ACP block-I/O substrate) -- keep in lockstep. +LIST="rms_core rms_io rms_seq rms_rel rms_idx rms_record rms_parse rms_search rms_util" OBJS="" for c in $LIST; do diff --git a/src/vmsrms/CMakeLists.txt b/src/vmsrms/CMakeLists.txt index 2963cee8a..9323ee709 100644 --- a/src/vmsrms/CMakeLists.txt +++ b/src/vmsrms/CMakeLists.txt @@ -50,6 +50,7 @@ endif() add_library(vmsrms ${OVMX_LIB_TYPE} rms_core.c + rms_io.c rms_seq.c rms_rel.c rms_idx.c @@ -76,6 +77,15 @@ if(VMSRMS_STANDALONE) else() target_link_libraries(vmsrms PUBLIC vms vmsfs vmslnm) + # vms-bc7: rms_io.c / rms_core.c reach the Files-11 ODS-2 ACP through the + # libvmssys /dev/vms wrappers (vms_kif_acp_*). vms_kif.h lives in libvmssys + # and pulls ../kernel/vms_ioctl.h -> vms_acp.h. Header-only compile dep -- + # the vms_kif_* symbols resolve through the transitive libvmssys link edge + # (vms -> vmsprocess -> libvmssys). NOT added on the netbsd-vax standalone + # cross, which keeps the POSIX backend (no /dev/vms). + target_include_directories(vmsrms PRIVATE + ${CMAKE_SOURCE_DIR}/src/libvmssys) + set_target_properties(vmsrms PROPERTIES PREFIX "" OUTPUT_NAME "LIBVMSRMS\$SHR" diff --git a/src/vmsrms/include/rms/fab.h b/src/vmsrms/include/rms/fab.h index cb73ee0ef..277ec0d7e 100644 --- a/src/vmsrms/include/rms/fab.h +++ b/src/vmsrms/include/rms/fab.h @@ -73,6 +73,7 @@ struct RAB; struct NAM; struct XABKEY; +struct rms_file; /* rms_io.h -- the ACP channel+window (or POSIX fd) handle */ struct FAB { uint8_t fab$b_bid; /* Block ID (must be 3) */ @@ -98,9 +99,17 @@ struct FAB { struct NAM *fab$l_nam; /* Name block address */ struct XABKEY *fab$l_xab; /* XAB chain address */ uint8_t fab$b_fsz; /* Fixed header size (VFC) */ - /* Internal state - not part of VMS FAB, used by our implementation */ - int _linux_fd; /* Underlying Linux file descriptor */ - char _resolved_path[1024]; /* Resolved Linux path */ + /* Internal state - not part of VMS FAB, used by our implementation. + * + * vms-bc7: the old `int _linux_fd` (a per-process POSIX file descriptor + + * positioned POSIX I/O) is RETIRED. RMS now reaches file data through the + * Files-11 ODS-2 ACP: _rms_file holds the executive channel $ASSIGNed to the + * mounted volume plus the ACCESSed file's VBN->LBN window (rms_io.h); record + * I/O rides IO$_READVBLK/IO$_WRITEVBLK over /dev/vms. (On the netbsd-vax + * standalone cross the same handle carries a POSIX fd until VAX's own ACP + * re-target, vms-d5d.) */ + struct rms_file *_rms_file; /* ACP channel+window handle (was _linux_fd) */ + char _resolved_path[1024]; /* Resolved VMS filespec (name.type;ver) */ void *_rms_state; /* Internal RMS state */ }; @@ -112,7 +121,7 @@ struct FAB { .fab$b_rfm = FAB$C_STMLF, \ .fab$b_rat = FAB$M_CR, \ .fab$b_fac = FAB$M_GET, \ - ._linux_fd = -1 \ + ._rms_file = 0 \ } #endif /* __RMS_FAB_H */ diff --git a/src/vmsrms/rms_core.c b/src/vmsrms/rms_core.c index c5f5480e8..5f7723a20 100644 --- a/src/vmsrms/rms_core.c +++ b/src/vmsrms/rms_core.c @@ -12,11 +12,12 @@ * OVMX userspace service register (rd vms-5b4) -- gate: * tests/integration/test_userspace_service_register.sh * - * Each service below works on the caller's own FAB/RAB; where it touches a - * file it does so through a plain path or through the fd cached in - * fab->_linux_fd, in this process. There is no executive file or record layer, - * so there is no cross-process record locking and no shared file access - * arbitration behind any of them. + * Each service below works on the caller's own FAB/RAB. Since vms-bc7 the file + * and block layer IS the executive's: file access rides the Files-11 ODS-2 ACP + * (channel + $QIO over /dev/vms, FAB._rms_file), not a per-process POSIX fd. + * What is still missing is cross-process RECORD locking / shared-access + * arbitration -- RMS runs in the caller's process (as it does on VMS too), but + * the RAB$M_ lock options do not yet reach the executive lock manager. * * THEY CITED vms-5b4 UNTIL vms-fab -- the item that BUILT this register, closed * when the register landed, owning none of the facades in it. vms-407 owns them @@ -25,27 +26,36 @@ * the arbitration the process context is supposed to CALL, and vms-ci.7 already * built the lock manager it should call. * - * FILESPEC RESOLUTION NOW REACHES THE EXECUTIVE (vms-96e2). sys$open/$create/ - * $erase take a VMS filespec and resolve it through vmsfs_to_linux_path -> - * lnm_translate -> vms_kif_lnm_translate, which reads the executive-resident - * LNM$SYSTEM table for system logical names (SYS$SYSDEVICE, SYS$UPDATE, ...). - * That step, and only that step, is the executive's; the RMS operation itself - * is still this process's. So they are PARTIAL, not wholly userspace. + * FILE ACCESS NOW REACHES THE EXECUTIVE THROUGH THE Files-11 ODS-2 ACP + * (vms-bc7, epic vms-208). sys$open/$create/$close/$erase/$extend $ASSIGN an + * executive channel to the mounted SYS$DISK, resolve the file by name to a FID, + * and issue the ACP $QIO file operations (IO$_ACCESS / IO$_CREATE / IO$_DEACCESS + * / IO$_DELETE / IO$_MODIFY) over /dev/vms -- there is no longer a per-process + * POSIX fd (FAB._linux_fd is retired). What stays this process's is the RMS + * bookkeeping: record-attribute defaults carried on the FAB, the RAB cursor, + * the IFI/ISI counters. So these are PARTIAL. (The netbsd-vax standalone cross + * keeps a POSIX backend until VAX's own ACP re-target, vms-d5d.) * - * OVMX-PARTIAL: sys$open (vms-96e2) -- exec: the VMS filespec is translated to a - * Linux path via the executive-resident LNM$SYSTEM table (vmsfs_to_linux_path - * -> lnm_translate -> vms_kif_lnm_translate). - * OVMX-LOCAL: sys$open -- the open itself is open(2) on the translated path; the - * FAB share/access fields do not reach any arbitrator. - * OVMX-PARTIAL: sys$create (vms-96e2) -- exec: same executive-resident filespec - * resolution as sys$open, before the file is created. - * OVMX-LOCAL: sys$create -- open(2) with O_CREAT, plus a .rms_meta sidecar - * written by this process. - * OVMX-USERSPACE: sys$close (vms-407) -- close(2) of the caller's own fd. - * OVMX-PARTIAL: sys$erase (vms-96e2) -- exec: same executive-resident filespec - * resolution as sys$open, before the file is removed. - * OVMX-LOCAL: sys$erase -- unlink(2) with no interlock against another process - * holding the file open. + * OVMX-PARTIAL: sys$open (vms-bc7) -- exec: $ASSIGN the volume + IO$_ACCESS + * resolves the filespec by name to a FID and builds the file's VBN->LBN + * window (rms_acp_open_file). + * OVMX-LOCAL: sys$open -- the FAB record-format fields and IFI are set in this + * process; the FAB share/access fields do not reach any arbitrator. + * OVMX-PARTIAL: sys$create (vms-bc7) -- exec: IO$_CREATE mints a real FID from + * INDEXF.SYS, enters a versioned directory record, and builds a write window. + * OVMX-LOCAL: sys$create -- the record-format/organization attributes are + * carried on the caller's FAB; no executive record lock is taken. + * OVMX-PARTIAL: sys$close (vms-bc7) -- exec: IO$_DEACCESS tears down the file + * window and $DASSGN releases the executive channel. + * OVMX-LOCAL: sys$close -- frees the RMS handle and clears the IFI in this + * process. + * OVMX-PARTIAL: sys$erase (vms-bc7) -- exec: $ASSIGN + IO$_DELETE removes the + * directory entry and deallocates the file's header and blocks. + * OVMX-LOCAL: sys$erase -- resolves the filespec fields in this process; no + * interlock against another accessor is taken here. + * OVMX-PARTIAL: sys$extend (vms-bc7) -- exec: IO$_MODIFY allocates fab$l_alq + * more blocks (BITMAP.SYS + FH2 retrieval-pointer append) without moving EOF. + * OVMX-LOCAL: sys$extend -- validates the caller's own FAB before the request. * OVMX-USERSPACE: sys$connect (vms-407) -- initializes the stream-position * fields (_current_offset/_eof/_last_rec_offset/_last_rec_size/rab$w_isi) * directly inside the caller's own RAB. Measured: it never allocates; @@ -54,13 +64,12 @@ * OVMX-USERSPACE: sys$disconnect (vms-407) -- resets those same fields and * frees rab->_rms_stream if non-NULL, which measurement shows is always * NULL -- no code path in the tree ever assigns it. - * OVMX-USERSPACE: sys$display (vms-407) -- reloads the .rms_meta sidecar this - * process wrote; the attributes are file content, not executive metadata. - * OVMX-USERSPACE: sys$rewind (vms-407) -- repositions the caller's own fd. - * OVMX-USERSPACE: sys$flush (vms-407) -- flushes the caller's own fd. - * OVMX-USERSPACE: sys$extend (vms-da9) -- validates the caller's own FAB and - * requires an open fd; the requested allocation is met by the Linux backing - * store's on-demand block allocation, so no executive allocator is called. + * OVMX-USERSPACE: sys$display (vms-407) -- fills XAB fields from FAB/handle + * state in this process; it issues no ACP $QIO. + * OVMX-USERSPACE: sys$rewind (vms-407) -- repositions the RAB byte cursor + * (rms_io_lseek is pure cursor arithmetic; no $QIO). + * OVMX-USERSPACE: sys$flush (vms-407) -- IO$_WRITEVBLK is already write-through, + * so the flush issues no $QIO (rms_io_fsync is a no-op on the ACP backend). */ #include @@ -75,11 +84,16 @@ #include #include "rms/rms.h" #include "rms_internal.h" +#include "rms_io.h" #include "vmsfs/filespec.h" #include "vmsfs/version.h" #include "ovmx_layout.h" #include "ssdef.h" #include "lib$routines.h" /* lib$cvt_vectim: Unix->VMS binary time (vms-3dd) */ +#if defined(__linux__) +#include "vms_kif.h" /* vms-bc7: the Files-11 ODS-2 ACP over /dev/vms */ +#include "vmsfs/ods2.h" /* ODS2_FK_* file-kind selectors for IO$_CREATE */ +#endif /* * unix_time_to_vms - Convert a Unix time_t (seconds since 1970-01-01 UTC) to a @@ -153,6 +167,254 @@ extern uint32_t rms_idx_cleanup(struct FAB *fab); /* Forward decl: rms_impl_open falls through to rms_impl_create on FAB$M_CIF, * which is defined later in this file. */ static uint32_t rms_impl_create(void *fab_ptr); +/* rms_resolve_spec (filespec + default merge) is defined further down; the ACP + * lifecycle helpers below use it to apply fab$l_dna defaults. */ +static int rms_resolve_spec(const char *spec, const char *default_spec, + char *out, size_t outlen); + +#if defined(__linux__) +/* ====================================================================== + * vms-bc7: Files-11 (ODS-2) ACP file lifecycle -- the product runtime. + * + * $OPEN/$CREATE $ASSIGN an executive channel to the mounted volume, resolve + * the directory FID, and IO$_ACCESS / IO$_CREATE the file (building the + * VBN->LBN window RMS record I/O then rides via rms_io_*). $CLOSE IO$_DEACCESS + * + $DASSGN; $ERASE IO$_DELETE; $EXTEND IO$_MODIFY. No POSIX file I/O, no + * sidecar, no vmsfs_to_linux_path -- fail-honest against a real /dev/vms + * (SS$_NOSUCHDEV / SS$_NOSUCHFILE / SS$_NOPRIV), never a silent local success + * (CLAUDE.md Rule 9 / INV-6). + * ====================================================================== */ + +/* The boot volume unit RMS $ASSIGNs when the filespec names no device. The + * discovered-SYS$DISK logical (device-native naming, epic vms-47d) is bound by + * the boot flip that ACP-mounts the system disk; until then the mounted unit is + * the boot unit DKA0:, which is also what the QEMU ACP fixtures mount. */ +#define RMS_ACP_DEFAULT_DEV "DKA0:" + +struct rms_acp_spec { + char devnam[16]; /* "DKA0:" (mounted unit to $ASSIGN) */ + char dirpath[256]; /* raw "A.B.C" inside [] (no brackets) */ + char name[VMS_ACP_NAME_SIZE];/* "NAME.TYP" upcased */ + uint16_t version; /* 0 => highest (open) / highest+1 (create) */ +}; + +/* Compose the effective VMS filespec from fab$l_fna (+ fab$l_dna defaults) and + * split it into device / directory / name.type / version. ODS-2 is case- + * insensitive; the name is upcased so it matches the on-disk directory records + * the codec decodes. Returns 0 on success, -1 on a malformed / empty spec. */ +static int rms_acp_spec_from_fab(struct FAB *fab, struct rms_acp_spec *s) +{ + char spec[1024] = ""; + const char *p; + const char *lb, *rb, *colon; + + if (!fab->fab$l_fna || fab->fab$b_fns == 0) + return -1; + + { + size_t len = fab->fab$b_fns; + if (len >= sizeof(spec)) len = sizeof(spec) - 1; + memcpy(spec, fab->fab$l_fna, len); + spec[len] = '\0'; + } + /* Apply a default filespec (fab$l_dna) for any missing name/type. */ + if (fab->fab$l_dna && fab->fab$b_dns > 0) { + char dflt[1024] = ""; + char combined[1024]; + size_t dlen = fab->fab$b_dns; + if (dlen >= sizeof(dflt)) dlen = sizeof(dflt) - 1; + memcpy(dflt, fab->fab$l_dna, dlen); + dflt[dlen] = '\0'; + if (rms_resolve_spec(spec, dflt, combined, sizeof(combined)) == 0) { + strncpy(spec, combined, sizeof(spec) - 1); + spec[sizeof(spec) - 1] = '\0'; + } + } + + memset(s, 0, sizeof(*s)); + strncpy(s->devnam, RMS_ACP_DEFAULT_DEV, sizeof(s->devnam) - 1); + s->version = 0; + + p = spec; + + /* DEVICE: text before the FIRST ':' that precedes any '[' (a logical name + * or physical unit). Store WITH the trailing ':' as $ASSIGN expects. */ + lb = strchr(spec, '['); + colon = strchr(spec, ':'); + if (colon && (!lb || colon < lb)) { + size_t dl = (size_t)(colon - spec) + 1; /* include ':' */ + if (dl < sizeof(s->devnam)) { + memcpy(s->devnam, spec, dl); + s->devnam[dl] = '\0'; + } + p = colon + 1; + } + + /* DIRECTORY: text inside [ ]. */ + lb = strchr(p, '['); + rb = lb ? strchr(lb, ']') : NULL; + if (lb && rb && rb > lb + 1) { + size_t dl = (size_t)(rb - lb - 1); + if (dl >= sizeof(s->dirpath)) dl = sizeof(s->dirpath) - 1; + memcpy(s->dirpath, lb + 1, dl); + s->dirpath[dl] = '\0'; + p = rb + 1; + } + + /* NAME.TYP;VER: the remainder. Split off ';version' first. */ + { + char rest[VMS_ACP_NAME_SIZE + 16] = ""; + const char *semi; + size_t rl; + strncpy(rest, p, sizeof(rest) - 1); + rest[sizeof(rest) - 1] = '\0'; + semi = strchr(rest, ';'); + if (semi) { + long v = 0; + const char *q = semi + 1; + /* ';' with no digits, or ';0', => highest; else the literal N. */ + for (; *q >= '0' && *q <= '9'; q++) + v = v * 10 + (*q - '0'); + s->version = (uint16_t)v; + rl = (size_t)(semi - rest); + } else { + rl = strlen(rest); + } + if (rl == 0) + return -1; /* no filename component */ + if (rl >= sizeof(s->name)) rl = sizeof(s->name) - 1; + for (size_t i = 0; i < rl; i++) { + char c = rest[i]; + if (c >= 'a' && c <= 'z') c = (char)(c - 'a' + 'A'); + s->name[i] = c; + } + s->name[rl] = '\0'; + } + return 0; +} + +/* Resolve the directory named by s->dirpath to its FID by walking each + * "COMP" as "COMP.DIR" from the MFD ([000000] == FID 4, addressed by an + * all-zero DID). Empty/[000000] dirpath => the MFD itself (all-zero DID). */ +static uint32_t rms_acp_resolve_did(uint32_t chan, const char *dirpath, + uint16_t *dn, uint16_t *ds, + uint8_t *dr, uint8_t *dx) +{ + char work[256]; + char *save = NULL, *tok; + + *dn = 0; *ds = 0; *dr = 0; *dx = 0; /* MFD */ + + if (!dirpath || dirpath[0] == '\0') + return SS$_NORMAL; + strncpy(work, dirpath, sizeof(work) - 1); + work[sizeof(work) - 1] = '\0'; + if (strcmp(work, "000000") == 0) + return SS$_NORMAL; + + for (tok = strtok_r(work, ".", &save); tok; + tok = strtok_r(NULL, ".", &save)) { + struct vms_acp_access_args a; + uint32_t st; + size_t tl = strlen(tok); + char nm[VMS_ACP_NAME_SIZE]; + + if (tl == 0) + continue; + if (tl > VMS_ACP_NAME_SIZE - 5) tl = VMS_ACP_NAME_SIZE - 5; + for (size_t i = 0; i < tl; i++) { + char c = tok[i]; + if (c >= 'a' && c <= 'z') c = (char)(c - 'a' + 'A'); + nm[i] = c; + } + nm[tl] = '\0'; + strncat(nm, ".DIR", sizeof(nm) - strlen(nm) - 1); + + memset(&a, 0, sizeof(a)); + a.chan = chan; + a.did_num = *dn; a.did_seq = *ds; a.did_rvn = *dr; a.did_nmx = *dx; + a.version = 1; /* directories are version ;1 */ + strncpy(a.name, nm, VMS_ACP_NAME_SIZE - 1); + + st = vms_kif_acp_access(&a); + if (!$VMS_STATUS_SUCCESS(st)) + return st; /* SS$_NOSUCHFILE etc -- honest */ + /* Chain: this directory's FID is the DID for the next component. */ + *dn = a.fid_num; *ds = a.fid_seq; *dr = a.fid_rvn; *dx = a.fid_nmx; + /* Release the transient directory access; we only wanted its FID. */ + vms_kif_acp_deaccess(chan); + } + return SS$_NORMAL; +} + +/* Map an ACP SS$_ status from the file-open path to the RMS $OPEN status. */ +static uint32_t rms_acp_open_status(uint32_t ss) +{ + switch (ss) { + case SS$_NOSUCHFILE: return RMS$_FNF; + case SS$_NOPRIV: return RMS$_PRV; + /* No SYS$DISK mounted / no /dev/vms: RMS$_ACC (file access error, ACP) + * -- fail-honest, never a silent local success (INV-6). */ + case SS$_NOSUCHDEV: return RMS$_ACC; + case SS$_DEVNOTMOUNT:return RMS$_ACC; + default: return RMS$_ACC; + } +} + +/* Seed the handle's cached EOF/HIBLK from an IO$_ACCESS/IO$_CREATE attr block. */ +static void rms_acp_seed_handle(rms_file_t *h, const struct vms_acp_fileattr *at) +{ + uint32_t efblk = at->efblk; + h->eof = (uint64_t)(efblk ? (efblk - 1u) : 0) * 512u + at->ffbyte; + h->hiblk = at->hiblk; +} + +/* $ASSIGN + resolve DID + IO$_ACCESS a file by name; fills *hp with a fresh + * handle on success. want_write selects read vs write access. */ +static uint32_t rms_acp_open_file(struct rms_acp_spec *s, int want_write, + rms_file_t **hp) +{ + rms_file_t *h; + struct vms_acp_access_args a; + uint32_t chan = 0, st; + + st = vms_kif_acp_assign(s->devnam, &chan); + if (!$VMS_STATUS_SUCCESS(st)) + return st; + + h = calloc(1, sizeof(*h)); + if (!h) { vms_kif_dassgn(chan); return SS$_INSFMEM; } + h->chan = chan; h->assigned = 1; h->fd = -1; + + memset(&a, 0, sizeof(a)); + a.chan = chan; + if (want_write) a.acctl = VMS_ACP_ACCTL_WRITE; + st = rms_acp_resolve_did(chan, s->dirpath, + &a.did_num, &a.did_seq, &a.did_rvn, &a.did_nmx); + if (!$VMS_STATUS_SUCCESS(st)) { free(h); vms_kif_dassgn(chan); return st; } + a.version = s->version; + strncpy(a.name, s->name, VMS_ACP_NAME_SIZE - 1); + + st = vms_kif_acp_access(&a); + if (!$VMS_STATUS_SUCCESS(st)) { free(h); vms_kif_dassgn(chan); return st; } + + h->accessed = 1; h->writable = want_write ? 1 : 0; + h->fid_num = a.fid_num; h->fid_seq = a.fid_seq; + h->fid_rvn = a.fid_rvn; h->fid_nmx = a.fid_nmx; + rms_acp_seed_handle(h, &a.attr); + *hp = h; + return SS$_NORMAL; +} + +/* IO$_DEACCESS + $DASSGN + free. */ +static void rms_acp_close_handle(rms_file_t *h) +{ + if (!h) return; + if (h->accessed) vms_kif_acp_deaccess(h->chan); + if (h->assigned) vms_kif_dassgn(h->chan); + free(h); +} +#endif /* __linux__ ACP lifecycle helpers */ struct rms_metadata { uint32_t magic; /* RMS_META_MAGIC */ @@ -679,6 +941,53 @@ static uint32_t rms_impl_open(void *fab_ptr) return RMS$_FAB; } +#if defined(__linux__) + /* --- Files-11 ODS-2 ACP path (vms-bc7): $ASSIGN + IO$_ACCESS --- */ + { + struct rms_acp_spec sp; + rms_file_t *h = NULL; + uint32_t st; + int need_write = ((fab->fab$b_fac & FAB$M_PUT) || + (fab->fab$b_fac & FAB$M_UPD) || + (fab->fab$b_fac & FAB$M_DEL) || + (fab->fab$b_fac & FAB$M_TRN)); + + if (fab->fab$b_org == FAB$C_IDX) { + /* Indexed-over-ACP (the ODS-2 prologue/bucket index) is a later + * rung of epic vms-208: the data fork rides the ACP, but the index + * has no ACP home yet. Fail-honest rather than silently serve an + * un-persistable index (INV-6). */ + fab->fab$l_sts = RMS$_ORG; + return RMS$_ORG; + } + if (rms_acp_spec_from_fab(fab, &sp) < 0) { + fab->fab$l_sts = RMS$_SYN; + fab->fab$l_stv = 0; + return RMS$_SYN; + } + strncpy(fab->_resolved_path, sp.name, sizeof(fab->_resolved_path) - 1); + fab->_resolved_path[sizeof(fab->_resolved_path) - 1] = '\0'; + + st = rms_acp_open_file(&sp, need_write, &h); + if (!$VMS_STATUS_SUCCESS(st)) { + if (st == SS$_NOSUCHFILE && (fab->fab$l_fop & FAB$M_CIF)) + return rms_impl_create(fab_ptr); + fab->fab$l_stv = st; + fab->fab$l_sts = rms_acp_open_status(st); + return fab->fab$l_sts; + } + fab->_rms_file = h; + + pthread_mutex_lock(&rms_id_lock); + fab->fab$w_ifi = next_ifi++; + if (next_ifi == 0) next_ifi = 1; + pthread_mutex_unlock(&rms_id_lock); + + fab->fab$l_sts = RMS$_NORMAL; + fab->fab$l_stv = 0; + return RMS$_NORMAL; + } +#else if (resolve_for_open(fab) < 0) { fab->fab$l_sts = RMS$_SYN; fab->fab$l_stv = 0; @@ -723,7 +1032,8 @@ static uint32_t rms_impl_open(void *fab_ptr) } } - fab->_linux_fd = fd; + fab->_rms_file = rms_io_posix_wrap(fd); + if (!fab->_rms_file) { close(fd); fab->fab$l_sts = RMS$_DME; return RMS$_DME; } load_metadata(fab); /* Assign an internal file identifier */ @@ -735,6 +1045,7 @@ static uint32_t rms_impl_open(void *fab_ptr) fab->fab$l_sts = RMS$_NORMAL; fab->fab$l_stv = 0; return RMS$_NORMAL; +#endif /* __linux__ */ } /* @@ -751,6 +1062,86 @@ static uint32_t rms_impl_create(void *fab_ptr) return RMS$_FAB; } +#if defined(__linux__) + /* --- Files-11 ODS-2 ACP path (vms-bc7): $ASSIGN + IO$_CREATE(+ACCESS) --- */ + { + struct rms_acp_spec sp; + rms_file_t *h; + struct vms_acp_fileop_args fop; + uint32_t chan = 0, st; + + if (fab->fab$b_org == FAB$C_IDX) { /* deferred: see rms_impl_open */ + fab->fab$l_sts = RMS$_ORG; + return RMS$_ORG; + } + if (rms_acp_spec_from_fab(fab, &sp) < 0) { + fab->fab$l_sts = RMS$_SYN; + return RMS$_SYN; + } + strncpy(fab->_resolved_path, sp.name, sizeof(fab->_resolved_path) - 1); + fab->_resolved_path[sizeof(fab->_resolved_path) - 1] = '\0'; + + st = vms_kif_acp_assign(sp.devnam, &chan); + if (!$VMS_STATUS_SUCCESS(st)) { + fab->fab$l_stv = st; + fab->fab$l_sts = rms_acp_open_status(st); + return fab->fab$l_sts; + } + h = calloc(1, sizeof(*h)); + if (!h) { vms_kif_dassgn(chan); fab->fab$l_sts = RMS$_DME; return RMS$_DME; } + h->chan = chan; h->assigned = 1; h->fd = -1; + + memset(&fop, 0, sizeof(fop)); + fop.chan = chan; + fop.func = VMS_ACP_FOP_CREATE; + /* IO$M_CREATE enters a versioned directory entry; IO$M_ACCESS builds + * the write window on this channel so record $PUTs ride it directly. */ + fop.modifiers = VMS_ACP_M_CREATE | VMS_ACP_M_ACCESS; + fop.acctl = VMS_ACP_ACCTL_WRITE; + fop.kind = (fab->fab$b_rfm == FAB$C_FIX) ? ODS2_FK_DATA_FIX + : ODS2_FK_DATA; + st = rms_acp_resolve_did(chan, sp.dirpath, &fop.did_num, &fop.did_seq, + &fop.did_rvn, &fop.did_nmx); + if (!$VMS_STATUS_SUCCESS(st)) { + free(h); vms_kif_dassgn(chan); + fab->fab$l_stv = st; fab->fab$l_sts = RMS$_DNF; + return RMS$_DNF; + } + fop.version = 0; /* highest existing + 1 */ + strncpy(fop.name, sp.name, VMS_ACP_NAME_SIZE - 1); + + st = vms_kif_acp_fileop(&fop); + if (!$VMS_STATUS_SUCCESS(st)) { + free(h); vms_kif_dassgn(chan); + fab->fab$l_stv = st; fab->fab$l_sts = RMS$_CRE; + return RMS$_CRE; + } + h->accessed = 1; h->writable = 1; + h->fid_num = fop.fid_num; h->fid_seq = fop.fid_seq; + h->fid_rvn = fop.fid_rvn; h->fid_nmx = fop.fid_nmx; + h->eof = (uint64_t)(fop.new_efblk ? (fop.new_efblk - 1u) : 0) * 512u + + fop.new_ffbyte; + h->hiblk = fop.new_hiblk; + fab->_rms_file = h; + + /* Relative files pre-allocate their fixed cells (IO$_MODIFY extend via + * rms_io_ftruncate); best-effort, exactly as the old POSIX path. */ + if (fab->fab$b_org == FAB$C_REL && fab->fab$l_mrn > 0 && + fab->fab$w_mrs > 0) { + size_t cell = (size_t)fab->fab$w_mrs + 1; + if (fab->fab$l_mrn <= SIZE_MAX / cell) + (void)rms_io_ftruncate(h, (off_t)(cell * fab->fab$l_mrn)); + } + + pthread_mutex_lock(&rms_id_lock); + fab->fab$w_ifi = next_ifi++; + if (next_ifi == 0) next_ifi = 1; + pthread_mutex_unlock(&rms_id_lock); + fab->fab$l_sts = RMS$_CREATED; + fab->fab$l_stv = 0; + return RMS$_NORMAL; + } +#else if (resolve_filename(fab) < 0) { fab->fab$l_sts = RMS$_SYN; return RMS$_SYN; @@ -812,7 +1203,8 @@ static uint32_t rms_impl_create(void *fab_ptr) return RMS$_CRE; } - fab->_linux_fd = fd; + fab->_rms_file = rms_io_posix_wrap(fd); + if (!fab->_rms_file) { close(fd); fab->fab$l_sts = RMS$_DME; return RMS$_DME; } /* Pre-allocate space for relative files */ if (fab->fab$b_org == FAB$C_REL && fab->fab$l_mrn > 0 && @@ -838,6 +1230,7 @@ static uint32_t rms_impl_create(void *fab_ptr) fab->fab$l_sts = RMS$_CREATED; fab->fab$l_stv = 0; return RMS$_NORMAL; +#endif /* __linux__ */ } /* @@ -855,32 +1248,50 @@ static uint32_t rms_impl_close(void *fab_ptr) } uint32_t close_sts = RMS$_NORMAL; - if (fab->_linux_fd >= 0) { - /* Flush before closing */ - if (fsync(fab->_linux_fd) < 0) { - close_sts = RMS$_WER; - } - close(fab->_linux_fd); - fab->_linux_fd = -1; - } - - /* Handle delete-on-close options */ int deleting = (fab->fab$l_fop & FAB$M_DLT) || (fab->fab$l_fop & FAB$M_TMD); - if (deleting) { - if (fab->_resolved_path[0]) { - unlink(fab->_resolved_path); - /* Also remove sidecar */ - char sidecar[1088]; - snprintf(sidecar, sizeof(sidecar), "%s%s", - fab->_resolved_path, RMS_SIDECAR_SUFFIX); - unlink(sidecar); - /* Remove index sidecar if present */ - char idxfile[1088]; - snprintf(idxfile, sizeof(idxfile), "%s%s", - fab->_resolved_path, RMS_INDEX_SUFFIX); - unlink(idxfile); + +#if defined(__linux__) + /* --- ACP path (vms-bc7): IO$_DEACCESS + $DASSGN (+ IO$_DELETE on DLT/TMD) --- */ + if (fab->_rms_file) { + rms_file_t *h = (rms_file_t *)fab->_rms_file; + rms_io_fsync(h); /* WRITEVBLK is write-through */ + if (deleting) { + /* Release the window, then deallocate the file by FID. */ + struct vms_acp_fileop_args fop; + if (h->accessed) { vms_kif_acp_deaccess(h->chan); h->accessed = 0; } + memset(&fop, 0, sizeof(fop)); + fop.chan = h->chan; + fop.func = VMS_ACP_FOP_DELETE; + fop.modifiers = VMS_ACP_M_DELETE; + fop.fidmode = 1; + fop.fid_num = h->fid_num; fop.fid_seq = h->fid_seq; + fop.fid_rvn = h->fid_rvn; fop.fid_nmx = h->fid_nmx; + (void)vms_kif_acp_fileop(&fop); } + rms_acp_close_handle(h); /* deaccess (if any) + dassgn + free */ + fab->_rms_file = NULL; + } +#else + /* --- POSIX path (netbsd-vax standalone; VAX re-targets under vms-d5d) --- */ + if (fab->_rms_file) { + int fd = rms_io_posix_fd((rms_file_t *)fab->_rms_file); + if (fd >= 0 && fsync(fd) < 0) + close_sts = RMS$_WER; + rms_io_posix_unwrap((rms_file_t *)fab->_rms_file); + fab->_rms_file = NULL; } + if (deleting && fab->_resolved_path[0]) { + unlink(fab->_resolved_path); + char sidecar[1088]; + snprintf(sidecar, sizeof(sidecar), "%s%s", + fab->_resolved_path, RMS_SIDECAR_SUFFIX); + unlink(sidecar); + char idxfile[1088]; + snprintf(idxfile, sizeof(idxfile), "%s%s", + fab->_resolved_path, RMS_INDEX_SUFFIX); + unlink(idxfile); + } +#endif /* * Clean up internal state. @@ -929,6 +1340,51 @@ static uint32_t rms_impl_erase(void *fab_ptr) return RMS$_FAB; } +#if defined(__linux__) + /* --- ACP path (vms-bc7): $ASSIGN + IO$_DELETE (remove entry + dealloc) --- */ + { + struct rms_acp_spec sp; + struct vms_acp_fileop_args fop; + uint32_t chan = 0, st; + + if (rms_acp_spec_from_fab(fab, &sp) < 0) { + fab->fab$l_sts = RMS$_SYN; + return RMS$_SYN; + } + st = vms_kif_acp_assign(sp.devnam, &chan); + if (!$VMS_STATUS_SUCCESS(st)) { + fab->fab$l_stv = st; + fab->fab$l_sts = rms_acp_open_status(st); + return fab->fab$l_sts; + } + memset(&fop, 0, sizeof(fop)); + fop.chan = chan; + fop.func = VMS_ACP_FOP_DELETE; + fop.modifiers = VMS_ACP_M_DELETE; /* remove entry AND deallocate */ + st = rms_acp_resolve_did(chan, sp.dirpath, &fop.did_num, &fop.did_seq, + &fop.did_rvn, &fop.did_nmx); + if (!$VMS_STATUS_SUCCESS(st)) { + vms_kif_dassgn(chan); + fab->fab$l_stv = st; fab->fab$l_sts = RMS$_DNF; + return RMS$_DNF; + } + fop.version = sp.version; /* 0 => all versions */ + strncpy(fop.name, sp.name, VMS_ACP_NAME_SIZE - 1); + + st = vms_kif_acp_fileop(&fop); + vms_kif_dassgn(chan); + if (!$VMS_STATUS_SUCCESS(st)) { + fab->fab$l_stv = st; + fab->fab$l_sts = (st == SS$_NOSUCHFILE) ? RMS$_FNF + : (st == SS$_NOPRIV) ? RMS$_PRV + : RMS$_ACC; + return fab->fab$l_sts; + } + fab->fab$l_sts = RMS$_NORMAL; + fab->fab$l_stv = 0; + return RMS$_NORMAL; + } +#else if (resolve_for_open(fab) < 0) { fab->fab$l_sts = RMS$_SYN; return RMS$_SYN; @@ -970,6 +1426,7 @@ static uint32_t rms_impl_erase(void *fab_ptr) fab->fab$l_sts = RMS$_NORMAL; fab->fab$l_stv = 0; return RMS$_NORMAL; +#endif /* __linux__ */ } /* @@ -991,7 +1448,7 @@ static uint32_t rms_impl_connect(void *rab_ptr) return RMS$_FAB; } - if (fab->_linux_fd < 0) { + if (!fab->_rms_file) { rab->rab$l_sts = RMS$_ACC; return RMS$_ACC; } @@ -1011,7 +1468,7 @@ static uint32_t rms_impl_connect(void *rab_ptr) /* If RAB$M_EOF is set, position to end of file */ if (rab->rab$l_rop & RAB$M_EOF) { - rab->_current_offset = lseek(fab->_linux_fd, 0, SEEK_END); + rab->_current_offset = rms_io_lseek(fab->_rms_file, 0, SEEK_END); if (rab->_current_offset < 0) rab->_current_offset = 0; } @@ -1111,12 +1568,12 @@ static uint32_t rms_impl_rewind(void *rab_ptr) } struct FAB *fab = rab->rab$l_fab; - if (!fab || fab->_linux_fd < 0) { + if (!fab || !fab->_rms_file) { rab->rab$l_sts = RMS$_ACC; return RMS$_ACC; } - lseek(fab->_linux_fd, 0, SEEK_SET); + rms_io_lseek(fab->_rms_file, 0, SEEK_SET); rab->_current_offset = 0; rab->_eof = 0; rab->_last_rec_offset = 0; @@ -1137,12 +1594,12 @@ static uint32_t rms_impl_flush(void *rab_ptr) } struct FAB *fab = rab->rab$l_fab; - if (!fab || fab->_linux_fd < 0) { + if (!fab || !fab->_rms_file) { rab->rab$l_sts = RMS$_ACC; return RMS$_ACC; } - if (fsync(fab->_linux_fd) < 0) { + if (rms_io_fsync(fab->_rms_file) < 0) { rab->rab$l_sts = RMS$_WER; rab->rab$l_stv = (uint32_t)errno; return RMS$_WER; @@ -1178,13 +1635,46 @@ static uint32_t rms_impl_extend(void *fab_ptr) if (!fab || fab->fab$b_bid != FAB$C_BID) { return RMS$_FAB; } - if (fab->_linux_fd < 0) { + if (!fab->_rms_file) { fab->fab$l_sts = RMS$_IFI; return RMS$_IFI; } +#if defined(__linux__) + /* $EXTEND -> IO$_MODIFY (vms-bc7): allocate fab$l_alq more blocks to the + * file WITHOUT moving EOF -- a real ODS-2 allocation (BITMAP.SYS + + * retrieval-pointer append), by FID on the accessed channel. A zero request + * is a no-op success. Fail-honest: a full volume is SS$_DEVICEFULL. */ + { + rms_file_t *h = (rms_file_t *)fab->_rms_file; + struct vms_acp_fileop_args fop; + uint32_t st; + + if (fab->fab$l_alq == 0) { + fab->fab$l_sts = RMS$_NORMAL; + return RMS$_NORMAL; + } + memset(&fop, 0, sizeof(fop)); + fop.chan = h->chan; + fop.func = VMS_ACP_FOP_MODIFY; + fop.fidmode = 1; + fop.fid_num = h->fid_num; fop.fid_seq = h->fid_seq; + fop.fid_rvn = h->fid_rvn; fop.fid_nmx = h->fid_nmx; + fop.exsz = fab->fab$l_alq; + st = vms_kif_acp_fileop(&fop); + if (!$VMS_STATUS_SUCCESS(st)) { + fab->fab$l_stv = st; + fab->fab$l_sts = RMS$_CRE; /* allocation/extend error */ + return RMS$_CRE; + } + h->hiblk = fop.new_hiblk; /* EOF unchanged; only allocation grows */ + fab->fab$l_sts = RMS$_NORMAL; + return RMS$_NORMAL; + } +#else fab->fab$l_sts = RMS$_NORMAL; return RMS$_NORMAL; +#endif } diff --git a/src/vmsrms/rms_idx.c b/src/vmsrms/rms_idx.c index 897e812e4..f030322ac 100644 --- a/src/vmsrms/rms_idx.c +++ b/src/vmsrms/rms_idx.c @@ -28,6 +28,7 @@ #include "rms/rms.h" #include "rms_internal.h" #include "rms_util.h" +#include "rms_io.h" /* B-tree order (max children per node) */ #define BTREE_ORDER 64 @@ -625,8 +626,8 @@ static btree_t *get_tree(struct FAB *fab) */ uint32_t rms_idx_get(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; if (!rab->rab$l_ubf || rab->rab$w_usz == 0) return RMS$_RAB; btree_t *tree = get_tree(fab); @@ -672,14 +673,14 @@ uint32_t rms_idx_get(struct FAB *fab, struct RAB *rab) if (!found) return RMS$_RNF; /* Read the record from the data file at the found offset */ - lseek(fd, rec_offset, SEEK_SET); + rms_io_lseek(fd, rec_offset, SEEK_SET); uint8_t status; - if (read(fd, &status, 1) != 1) return RMS$_RER; + if (rms_io_read(fd, &status, 1) != 1) return RMS$_RER; if (status != IDX_REC_ACTIVE) return RMS$_DEL; uint16_t reclen; - if (rms_read_exact(fd, &reclen, 2) < 2) return RMS$_RER; + if (rms_io_read_exact(fd, &reclen, 2) < 2) return RMS$_RER; if (reclen > rab->rab$w_usz) { rab->rab$l_stv = reclen; @@ -687,7 +688,7 @@ uint32_t rms_idx_get(struct FAB *fab, struct RAB *rab) } if (reclen > 0) { - if (rms_read_exact(fd, rab->rab$l_ubf, reclen) < reclen) { + if (rms_io_read_exact(fd, rab->rab$l_ubf, reclen) < reclen) { return RMS$_RER; } } @@ -695,33 +696,33 @@ uint32_t rms_idx_get(struct FAB *fab, struct RAB *rab) rab->rab$w_rsz = reclen; rab->_last_rec_offset = rec_offset; rab->_last_rec_size = reclen; - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); return RMS$_NORMAL; } else { /* Sequential access - scan data file */ - lseek(fd, rab->_current_offset, SEEK_SET); + rms_io_lseek(fd, rab->_current_offset, SEEK_SET); for (;;) { - off_t pos = lseek(fd, 0, SEEK_CUR); + off_t pos = rms_io_lseek(fd, 0, SEEK_CUR); uint8_t status; - ssize_t n = read(fd, &status, 1); + ssize_t n = rms_io_read(fd, &status, 1); if (n <= 0) return RMS$_EOF; uint16_t reclen; - if (rms_read_exact(fd, &reclen, 2) < 2) return RMS$_EOF; + if (rms_io_read_exact(fd, &reclen, 2) < 2) return RMS$_EOF; if (status == IDX_REC_ACTIVE) { if (reclen > rab->rab$w_usz) { /* Skip this record */ - lseek(fd, reclen, SEEK_CUR); - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rms_io_lseek(fd, reclen, SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); continue; } if (reclen > 0) { - if (rms_read_exact(fd, rab->rab$l_ubf, reclen) < reclen) { + if (rms_io_read_exact(fd, rab->rab$l_ubf, reclen) < reclen) { return RMS$_RER; } } @@ -729,12 +730,12 @@ uint32_t rms_idx_get(struct FAB *fab, struct RAB *rab) rab->rab$w_rsz = reclen; rab->_last_rec_offset = pos; rab->_last_rec_size = reclen; - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); return RMS$_NORMAL; } /* Skip deleted/inactive record */ - lseek(fd, reclen, SEEK_CUR); + rms_io_lseek(fd, reclen, SEEK_CUR); } } } @@ -757,8 +758,8 @@ uint32_t rms_idx_get(struct FAB *fab, struct RAB *rab) */ uint32_t rms_idx_put(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; char *buf = rab->rab$l_rbf ? rab->rab$l_rbf : rab->rab$l_ubf; if (!buf) return RMS$_RAB; @@ -775,13 +776,13 @@ uint32_t rms_idx_put(struct FAB *fab, struct RAB *rab) if (key_len == 0) return RMS$_KEY; /* Append record to data file */ - off_t rec_offset = lseek(fd, 0, SEEK_END); + off_t rec_offset = rms_io_lseek(fd, 0, SEEK_END); uint8_t status = IDX_REC_ACTIVE; - if (rms_write_exact(fd, &status, 1) < 0) return RMS$_WER; - if (rms_write_exact(fd, &len, 2) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, &status, 1) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, &len, 2) < 0) return RMS$_WER; if (len > 0) { - if (rms_write_exact(fd, buf, len) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, buf, len) < 0) return RMS$_WER; } /* Insert key into B-tree */ @@ -797,9 +798,9 @@ uint32_t rms_idx_put(struct FAB *fab, struct RAB *rab) int rc = btree_insert(tree, tree->root, key, key_len, rec_offset); if (rc == -1) { /* Duplicate key - we already wrote the record, so mark it deleted */ - lseek(fd, rec_offset, SEEK_SET); + rms_io_lseek(fd, rec_offset, SEEK_SET); uint8_t del = IDX_REC_DELETED; - rms_write_exact(fd, &del, 1); + rms_io_write_exact(fd, &del, 1); return RMS$_DUP; } @@ -813,7 +814,7 @@ uint32_t rms_idx_put(struct FAB *fab, struct RAB *rab) rab->_last_rec_offset = rec_offset; rab->_last_rec_size = len; - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); return RMS$_NORMAL; } @@ -831,8 +832,8 @@ uint32_t rms_idx_put(struct FAB *fab, struct RAB *rab) */ uint32_t rms_idx_delete(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; if (rab->_last_rec_offset == 0 && rab->_last_rec_size == 0) { return RMS$_CUR; @@ -842,15 +843,15 @@ uint32_t rms_idx_delete(struct FAB *fab, struct RAB *rab) if (!tree) return RMS$_DME; /* Read the record to get its key (for B-tree removal) */ - lseek(fd, rab->_last_rec_offset + 1, SEEK_SET); /* Skip status */ + rms_io_lseek(fd, rab->_last_rec_offset + 1, SEEK_SET); /* Skip status */ uint16_t reclen; - if (rms_read_exact(fd, &reclen, 2) < 2) return RMS$_RER; + if (rms_io_read_exact(fd, &reclen, 2) < 2) return RMS$_RER; char *rec_buf = NULL; if (reclen > 0) { rec_buf = malloc(reclen); if (!rec_buf) return RMS$_DME; - if (rms_read_exact(fd, rec_buf, reclen) < reclen) { + if (rms_io_read_exact(fd, rec_buf, reclen) < reclen) { free(rec_buf); return RMS$_RER; } @@ -869,9 +870,9 @@ uint32_t rms_idx_delete(struct FAB *fab, struct RAB *rab) } /* Mark record as deleted in data file */ - lseek(fd, rab->_last_rec_offset, SEEK_SET); + rms_io_lseek(fd, rab->_last_rec_offset, SEEK_SET); uint8_t del = IDX_REC_DELETED; - if (rms_write_exact(fd, &del, 1) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, &del, 1) < 0) return RMS$_WER; tree->num_records--; tree->dirty = 1; @@ -894,8 +895,8 @@ uint32_t rms_idx_delete(struct FAB *fab, struct RAB *rab) */ uint32_t rms_idx_update(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; char *buf = rab->rab$l_rbf ? rab->rab$l_rbf : rab->rab$l_ubf; if (!buf) return RMS$_RAB; @@ -915,14 +916,14 @@ uint32_t rms_idx_update(struct FAB *fab, struct RAB *rab) extract_key(buf, new_len, fab, new_key, &new_key_len); /* Read old record to get old key */ - lseek(fd, rab->_last_rec_offset + 1, SEEK_SET); + rms_io_lseek(fd, rab->_last_rec_offset + 1, SEEK_SET); uint16_t old_len; - if (rms_read_exact(fd, &old_len, 2) < 2) return RMS$_RER; + if (rms_io_read_exact(fd, &old_len, 2) < 2) return RMS$_RER; char *old_buf = malloc(old_len > 0 ? old_len : 1); if (!old_buf) return RMS$_DME; if (old_len > 0) { - if (rms_read_exact(fd, old_buf, old_len) < old_len) { + if (rms_io_read_exact(fd, old_buf, old_len) < old_len) { free(old_buf); return RMS$_RER; } @@ -946,16 +947,16 @@ uint32_t rms_idx_update(struct FAB *fab, struct RAB *rab) btree_remove(tree, tree->root, old_key, old_key_len, tree->key_dtp); /* Mark old record as deleted */ - lseek(fd, rab->_last_rec_offset, SEEK_SET); + rms_io_lseek(fd, rab->_last_rec_offset, SEEK_SET); uint8_t del = IDX_REC_DELETED; - rms_write_exact(fd, &del, 1); + rms_io_write_exact(fd, &del, 1); /* Append new record */ - off_t new_offset = lseek(fd, 0, SEEK_END); + off_t new_offset = rms_io_lseek(fd, 0, SEEK_END); uint8_t status = IDX_REC_ACTIVE; - rms_write_exact(fd, &status, 1); - rms_write_exact(fd, &new_len, 2); - if (new_len > 0) rms_write_exact(fd, buf, new_len); + rms_io_write_exact(fd, &status, 1); + rms_io_write_exact(fd, &new_len, 2); + if (new_len > 0) rms_io_write_exact(fd, buf, new_len); /* Handle root split */ if (tree->root->num_keys >= BTREE_MAX_KEYS) { @@ -972,19 +973,19 @@ uint32_t rms_idx_update(struct FAB *fab, struct RAB *rab) /* Key unchanged - can we rewrite in place? */ if (new_len == old_len) { /* Same size - rewrite in place */ - lseek(fd, rab->_last_rec_offset + 3, SEEK_SET); /* skip status + len */ - if (rms_write_exact(fd, buf, new_len) < 0) return RMS$_WER; + rms_io_lseek(fd, rab->_last_rec_offset + 3, SEEK_SET); /* skip status + len */ + if (rms_io_write_exact(fd, buf, new_len) < 0) return RMS$_WER; } else { /* Different size - delete and append */ - lseek(fd, rab->_last_rec_offset, SEEK_SET); + rms_io_lseek(fd, rab->_last_rec_offset, SEEK_SET); uint8_t del = IDX_REC_DELETED; - rms_write_exact(fd, &del, 1); + rms_io_write_exact(fd, &del, 1); - off_t new_offset = lseek(fd, 0, SEEK_END); + off_t new_offset = rms_io_lseek(fd, 0, SEEK_END); uint8_t status = IDX_REC_ACTIVE; - rms_write_exact(fd, &status, 1); - rms_write_exact(fd, &new_len, 2); - if (new_len > 0) rms_write_exact(fd, buf, new_len); + rms_io_write_exact(fd, &status, 1); + rms_io_write_exact(fd, &new_len, 2); + if (new_len > 0) rms_io_write_exact(fd, buf, new_len); /* Update B-tree offset */ btree_remove(tree, tree->root, new_key, new_key_len, tree->key_dtp); @@ -1017,8 +1018,8 @@ uint32_t rms_idx_update(struct FAB *fab, struct RAB *rab) */ uint32_t rms_idx_find(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; btree_t *tree = get_tree(fab); if (!tree) return RMS$_DME; @@ -1048,9 +1049,9 @@ uint32_t rms_idx_find(struct FAB *fab, struct RAB *rab) rab->_current_offset = rec_offset; /* Read the record length to set _last_rec_size */ - lseek(fd, rec_offset + 1, SEEK_SET); /* Skip status */ + rms_io_lseek(fd, rec_offset + 1, SEEK_SET); /* Skip status */ uint16_t reclen; - if (rms_read_exact(fd, &reclen, 2) >= 2) { + if (rms_io_read_exact(fd, &reclen, 2) >= 2) { rab->_last_rec_size = reclen; } diff --git a/src/vmsrms/rms_io.c b/src/vmsrms/rms_io.c new file mode 100644 index 000000000..13d3ed75b --- /dev/null +++ b/src/vmsrms/rms_io.c @@ -0,0 +1,337 @@ +/* + * rms_io.c - RMS block-I/O substrate (vms-bc7, epic vms-208). + * + * Implements the positioned-I/O vocabulary rms_io.h declares -- the POSIX-fd + * cursor semantics the seq/rel/idx record engines depend on -- over TWO + * backends behind one interface, so the record engines are substrate-agnostic: + * + * __linux__ : the Files-11 (ODS-2) ACP. A record at {byte-offset, length} + * becomes {VBN, byte-offset, length} IO$_READVBLK/IO$_WRITEVBLK + * on an ACP file-class channel window, ridden over /dev/vms via + * vms_kif_acp_readvb / vms_kif_acp_writevb / vms_kif_acp_fileop. + * Fail-honest, NO POSIX fallback (CLAUDE.md Rule 9 / INV-6). + * + * otherwise : ordinary POSIX (pread/pwrite/ftruncate on a bare fd held in + * the handle). This is the netbsd-vax standalone cross's path + * (src/vmsrms/CMakeLists.txt: "RMS file I/O is ordinary POSIX"); + * VAX keeps it until its own ACP re-target (vms-d5d). The FAB no + * longer carries _linux_fd on any platform -- the fd now lives + * inside the rms_file handle. + * + * ACP byte/VBN model (src/kernel-core/vmsfs_acp.c, src/kernel/vms_acp.h): + * - A file's VALID byte length is (efblk-1)*512 + ffbyte; the handle mirrors + * it in f->eof, seeded at ACCESS/CREATE and grown on every write. + * - IO$_READVBLK clamps at valid bytes (a read at/after EOF is SS$_ENDOFFILE + * with 0 transferred -> POSIX read() returning 0 at EOF). + * - IO$_WRITEVBLK past EOF implicitly extends (BITMAP.SYS alloc + FH2 grow) + * and sets valid = end-of-write -> POSIX write() growing the file. + * A byte cursor C maps to vbn = C/512 + 1, offset = C%512; the ACP handles a + * length spanning block boundaries in one call (up to ACP_RW_MAX_XFER = 1 MiB, + * so a larger transfer loops here -- record I/O never approaches it). + */ + +#include + +#include "rms_io.h" + +#ifndef SEEK_SET +#define SEEK_SET 0 +#define SEEK_CUR 1 +#define SEEK_END 2 +#endif + +#define RMS_IO_BLK 512u + +/* ========================================================================== + * lseek / fsync are backend-independent (cursor arithmetic; write-through). + * ========================================================================== */ + +off_t rms_io_lseek(rms_file_t *f, off_t offset, int whence) +{ + uint64_t base; + + if (!f) + return (off_t)-1; + + switch (whence) { + case SEEK_SET: base = 0; break; + case SEEK_CUR: base = f->cursor; break; + case SEEK_END: base = f->eof; break; + default: return (off_t)-1; + } + + if (offset < 0 && (uint64_t)(-offset) > base) + return (off_t)-1; /* negative position rejected */ + + f->cursor = base + (uint64_t)offset; + return (off_t)f->cursor; +} + +#if defined(__linux__) +/* ========================================================================== + * Backend A: Files-11 ODS-2 ACP (the product runtime, Rule 9). + * ========================================================================== */ + +#include "vms_kif.h" /* pulls ../kernel/vms_ioctl.h -> vms_acp.h structs */ +#include "ssdef.h" + +#define RMS_IO_MAX_XFER (1u << 20) /* mirror ACP_RW_MAX_XFER (1 MiB) */ + +ssize_t rms_io_read(rms_file_t *f, void *buf, size_t n) +{ + uint8_t *p = (uint8_t *)buf; + size_t done = 0; + + if (!f || !f->accessed) + return -1; + if (n == 0) + return 0; + + while (done < n) { + struct vms_acp_rw_args a; + uint32_t chunk = (n - done) > RMS_IO_MAX_XFER + ? RMS_IO_MAX_XFER : (uint32_t)(n - done); + uint32_t st; + + memset(&a, 0, sizeof(a)); + a.chan = f->chan; + a.vbn = (uint32_t)(f->cursor / RMS_IO_BLK) + 1u; + a.offset = (uint32_t)(f->cursor % RMS_IO_BLK); + a.length = chunk; + a.buffer = (uint64_t)(uintptr_t)(p + done); + + st = vms_kif_acp_readvb(&a); + if (st == SS$_ENDOFFILE) { + f->eof = (uint64_t)(a.new_efblk ? (a.new_efblk - 1u) : 0) * RMS_IO_BLK; + break; /* at/after EOF: return what we have */ + } + if (!$VMS_STATUS_SUCCESS(st)) + return -1; + if (a.xferred == 0) + break; + + f->cursor += a.xferred; + done += a.xferred; + if (a.new_hiblk) + f->hiblk = a.new_hiblk; + if (a.xferred < chunk) + break; /* short read == hit EOF this chunk */ + } + + return (ssize_t)done; +} + +ssize_t rms_io_write(rms_file_t *f, const void *buf, size_t n) +{ + const uint8_t *p = (const uint8_t *)buf; + size_t done = 0; + + if (!f || !f->accessed || !f->writable) + return -1; + if (n == 0) + return 0; + + while (done < n) { + struct vms_acp_rw_args a; + uint32_t chunk = (n - done) > RMS_IO_MAX_XFER + ? RMS_IO_MAX_XFER : (uint32_t)(n - done); + uint32_t st; + + memset(&a, 0, sizeof(a)); + a.chan = f->chan; + a.vbn = (uint32_t)(f->cursor / RMS_IO_BLK) + 1u; + a.offset = (uint32_t)(f->cursor % RMS_IO_BLK); + a.length = chunk; + a.buffer = (uint64_t)(uintptr_t)(p + done); + + st = vms_kif_acp_writevb(&a); + if (!$VMS_STATUS_SUCCESS(st)) + return done ? (ssize_t)done : -1; + + f->cursor += a.xferred; + done += a.xferred; + f->hiblk = a.new_hiblk; + if (f->cursor > f->eof) + f->eof = f->cursor; /* ACP set valid = end-of-write */ + if (a.xferred < chunk) + return done ? (ssize_t)done : -1; + } + + return (ssize_t)done; +} + +int rms_io_ftruncate(rms_file_t *f, off_t length) +{ + if (!f || !f->accessed || length < 0) + return -1; + if ((uint64_t)length == f->eof) + return 0; + + if ((uint64_t)length > f->eof) { + /* Grow: a single zero byte at the last position; IO$_WRITEVBLK's + * implicit extend allocates + zero-fills the shortfall and sets valid + * to the new length -- ftruncate() growth, ACP-only (relative-file + * cell pre-allocation). */ + uint64_t save = f->cursor; + uint8_t zero = 0; + ssize_t r; + f->cursor = (uint64_t)length - 1u; + r = rms_io_write(f, &zero, 1); + f->cursor = save; + return (r == 1) ? 0 : -1; + } + + /* Shrink: IO$_MODIFY truncate to the new EOF VBN/first-free-byte, by FID. */ + { + struct vms_acp_fileop_args a; + uint32_t st; + uint32_t efblk = (uint32_t)(((uint64_t)length + RMS_IO_BLK - 1) / RMS_IO_BLK); + uint16_t ffbyte = (uint16_t)((uint64_t)length - + (uint64_t)(efblk ? (efblk - 1u) : 0) * RMS_IO_BLK); + + memset(&a, 0, sizeof(a)); + a.chan = f->chan; + a.func = VMS_ACP_FOP_MODIFY; + a.fidmode = 1; + a.fid_num = f->fid_num; + a.fid_seq = f->fid_seq; + a.fid_rvn = f->fid_rvn; + a.fid_nmx = f->fid_nmx; + a.trunc_efblk = efblk ? efblk : 1u; + a.trunc_ffbyte = ffbyte; + + st = vms_kif_acp_fileop(&a); + if (!$VMS_STATUS_SUCCESS(st)) + return -1; + f->eof = (uint64_t)length; + f->hiblk = a.new_hiblk; + return 0; + } +} + +int rms_io_fsync(rms_file_t *f) +{ + /* IO$_WRITEVBLK is write-through (the ACP writes each block synchronously), + * so there is nothing buffered to flush. */ + (void)f; + return 0; +} + +#else +/* ========================================================================== + * Backend B: POSIX (netbsd-vax standalone cross; VAX re-targets under vms-d5d). + * The fd lives in the handle; positioned I/O uses the cursor so the engines' + * lseek+read/write idiom maps to pread/pwrite without an implicit kernel fd + * position (which multiple RABs on one FAB would otherwise race). + * ========================================================================== */ + +#include + +ssize_t rms_io_read(rms_file_t *f, void *buf, size_t n) +{ + ssize_t r; + if (!f || f->fd < 0) + return -1; + r = pread(f->fd, buf, n, (off_t)f->cursor); + if (r > 0) + f->cursor += (uint64_t)r; + return r; +} + +ssize_t rms_io_write(rms_file_t *f, const void *buf, size_t n) +{ + ssize_t r; + if (!f || f->fd < 0) + return -1; + r = pwrite(f->fd, buf, n, (off_t)f->cursor); + if (r > 0) { + f->cursor += (uint64_t)r; + if (f->cursor > f->eof) + f->eof = f->cursor; + } + return r; +} + +int rms_io_ftruncate(rms_file_t *f, off_t length) +{ + if (!f || f->fd < 0 || length < 0) + return -1; + if (ftruncate(f->fd, length) < 0) + return -1; + f->eof = (uint64_t)length; + return 0; +} + +int rms_io_fsync(rms_file_t *f) +{ + if (!f || f->fd < 0) + return -1; + return fsync(f->fd); +} + +#include + +rms_file_t *rms_io_posix_wrap(int fd) +{ + rms_file_t *f = (rms_file_t *)calloc(1, sizeof(*f)); + off_t end; + if (!f) + return NULL; + f->fd = fd; + f->accessed = 1; + f->writable = 1; + end = lseek(fd, 0, SEEK_END); + f->eof = (end > 0) ? (uint64_t)end : 0; + f->cursor = 0; + return f; +} + +void rms_io_posix_unwrap(rms_file_t *f) +{ + if (!f) + return; + if (f->fd >= 0) + close(f->fd); + free(f); +} + +int rms_io_posix_fd(rms_file_t *f) +{ + return f ? f->fd : -1; +} + +#endif /* backend select */ + +/* ========================================================================== + * *_exact wrappers (backend-independent -- built on rms_io_read/write above). + * ========================================================================== */ + +ssize_t rms_io_read_exact(rms_file_t *f, void *buf, size_t count) +{ + size_t total = 0; + uint8_t *p = (uint8_t *)buf; + + while (total < count) { + ssize_t n = rms_io_read(f, p + total, count - total); + if (n < 0) + return -1; + if (n == 0) + break; /* EOF */ + total += (size_t)n; + } + return (ssize_t)total; +} + +int rms_io_write_exact(rms_file_t *f, const void *buf, size_t count) +{ + size_t total = 0; + const uint8_t *p = (const uint8_t *)buf; + + while (total < count) { + ssize_t n = rms_io_write(f, p + total, count - total); + if (n <= 0) + return -1; + total += (size_t)n; + } + return 0; +} diff --git a/src/vmsrms/rms_io.h b/src/vmsrms/rms_io.h new file mode 100644 index 000000000..48904085d --- /dev/null +++ b/src/vmsrms/rms_io.h @@ -0,0 +1,112 @@ +/* + * rms_io.h - RMS block-I/O substrate over the Files-11 (ODS-2) ACP (vms-bc7, + * epic vms-208). + * + * THE SUBSTRATE SWAP. RMS used to reach file data through a per-process Linux + * file descriptor (FAB._linux_fd) with positioned POSIX I/O (lseek/read/write). + * That is gone. RMS now reaches file data the VMS way: a channel $ASSIGNed to + * the mounted ODS-2 volume, a file ACCESSed on that channel (IO$_ACCESS / + * IO$_CREATE builds the VBN->LBN retrieval-pointer window), and virtual-block + * transfers (IO$_READVBLK / IO$_WRITEVBLK) through that window -- all in the + * executive, over /dev/vms (src/libvmssys/vms_kif.c acp wrappers). + * + * The seq/rel/idx record engines are UNCHANGED in their record logic: they + * still think in file byte offsets (rab->_current_offset et al.) and still call + * a small "positioned I/O" vocabulary. This header re-homes exactly that + * vocabulary from a bare `int fd` onto an `rms_file_t *` handle whose backing is + * the ACP channel+window. rms_io_lseek/read/write/read_exact/write_exact + * reproduce the POSIX fd cursor semantics (a byte cursor the engines set with + * lseek and advance with read/write) on top of {VBN, byte-offset, length} + * READVBLK/WRITEVBLK. This is the design's "positioned-I/O sub-project, done the + * VMS way (virtual-block QIO)" (docs/design-files11-acp-executive.md §4.5). + * + * FAIL-HONEST (CLAUDE.md Rule 9 / INV-6). Every primitive rides + * vms_kif_acp_*, which returns SS$_NOSUCHDEV when /dev/vms is absent and the + * real SS$_ status otherwise -- there is NO POSIX fallback. A read/write on a + * handle with no accessed file, or against an absent executive, fails; it never + * silently succeeds against a private substitute. + */ +#ifndef RMS_IO_H +#define RMS_IO_H + +#include +#include +#include + +/* + * An RMS accessed file: the executive channel $ASSIGNed to the volume plus the + * state that lets the record engines treat it like a POSIX fd. Created by + * rms_core (rms_impl_open / rms_impl_create) via the ACP, stored on + * FAB._rms_file, and torn down by rms_impl_close. The record engines only ever + * read `cursor`/`eof` indirectly through the rms_io_* primitives below. + */ +typedef struct rms_file { + uint32_t chan; /* executive channel ($ASSIGN of the mounted volume) */ + int assigned; /* 1 once the channel is $ASSIGNed */ + int accessed; /* 1 once IO$_ACCESS / IO$_CREATE built a window */ + int writable; /* 1 if accessed for write */ + uint64_t cursor; /* byte cursor (emulates the POSIX fd position) */ + uint64_t eof; /* valid byte length (efblk/ffbyte-derived) */ + uint32_t hiblk; /* highest allocated VBN */ + /* FID of the accessed file (IO$_DELETE by-FID, diagnostics). */ + uint16_t fid_num; + uint16_t fid_seq; + uint8_t fid_rvn; + uint8_t fid_nmx; + /* POSIX backend (non-__linux__ / netbsd-vax standalone cross) only: the + * bare fd the handle now owns in place of the retired FAB._linux_fd. Unused + * by the ACP backend. */ + int fd; +} rms_file_t; + +/* + * Positioned-I/O vocabulary. Each mirrors the POSIX call the record engines + * used to make on the bare fd, but translates the byte cursor into + * {VBN, byte-offset, length} transfers on the ACP channel window. + */ + +/* lseek(2) emulation: move the byte cursor. whence is SEEK_SET/CUR/END; END is + * relative to the file's valid byte length (f->eof). Returns the new absolute + * byte position, or (off_t)-1 on a bad handle. */ +off_t rms_io_lseek(rms_file_t *f, off_t offset, int whence); + +/* read(2) emulation: read up to `n` bytes at the cursor via IO$_READVBLK, + * advance the cursor by the count returned. Returns the byte count (0 at EOF), + * or -1 on error (an executive/channel failure, never a POSIX fallback). */ +ssize_t rms_io_read(rms_file_t *f, void *buf, size_t n); + +/* write(2) emulation: write `n` bytes at the cursor via IO$_WRITEVBLK (which + * implicitly extends past EOF), advance the cursor, grow f->eof. Returns the + * byte count written, or -1 on error. */ +ssize_t rms_io_write(rms_file_t *f, const void *buf, size_t n); + +/* Read exactly `count` bytes (short only at EOF). Mirrors the old + * rms_read_exact(fd,...). Returns bytes read (< count only at EOF) or -1. */ +ssize_t rms_io_read_exact(rms_file_t *f, void *buf, size_t count); + +/* Write exactly `count` bytes. Mirrors the old rms_write_exact(fd,...). + * Returns 0 on success, -1 on error. */ +int rms_io_write_exact(rms_file_t *f, const void *buf, size_t count); + +/* ftruncate(2) emulation: set the file's valid byte length to `length`. + * Growing pre-allocates+zero-fills through the ACP (implicit extend); shrinking + * issues IO$_MODIFY truncate. Returns 0 on success, -1 on error. Used for + * relative-file cell pre-allocation. */ +int rms_io_ftruncate(rms_file_t *f, off_t length); + +/* fsync(2) emulation: IO$_WRITEVBLK is write-through to the block device + * (the ACP writes each block synchronously), so there is nothing buffered to + * flush. Returns 0. Present so the record engines keep a symmetric vocabulary. */ +int rms_io_fsync(rms_file_t *f); + +#if !defined(__linux__) +/* POSIX-backend (netbsd-vax standalone cross) only: wrap a freshly open(2)'d fd + * in a handle (eof seeded from the file's current size), release it (close + + * free), and read back the fd. The __linux__ ACP build has no fd -- the FAB + * lifecycle there $ASSIGNs/IO$_ACCESSes instead. */ +rms_file_t *rms_io_posix_wrap(int fd); +void rms_io_posix_unwrap(rms_file_t *f); /* close(fd) + free(handle) */ +int rms_io_posix_fd(rms_file_t *f); +#endif + +#endif /* RMS_IO_H */ diff --git a/src/vmsrms/rms_record.c b/src/vmsrms/rms_record.c index da90a07b0..02ba4b228 100644 --- a/src/vmsrms/rms_record.c +++ b/src/vmsrms/rms_record.c @@ -12,22 +12,35 @@ * tests/integration/test_userspace_service_register.sh * * Each of the five validates the caller's RAB/FAB and dispatches to an - * organization handler (rms_seq.c / rms_rel.c / rms_idx.c) that reads and - * writes through fab->_linux_fd, this process's own descriptor. RAB$M_ options - * that request record locking have no executive lock manager behind them here, - * so two processes reading and writing the same file are not serialised by - * anything RMS does. + * organization handler (rms_seq.c / rms_rel.c / rms_idx.c). Since vms-bc7 those + * handlers reach file data through the Files-11 ODS-2 ACP: the block-I/O + * substrate (rms_io.c) turns each positioned record transfer into IO$_READVBLK / + * IO$_WRITEVBLK on the file's channel window over /dev/vms -- NOT a per-process + * POSIX fd. The RECORD framing (RFM/RAT decode, key compares) is still done in + * this process; only the block I/O beneath it is the executive's. RAB$M_ record + * locking still has no executive lock manager behind it here (vms-407 owns that + * missing arbitration, across rms_core.c, this file and rms_search.c). * - * These five cited vms-5b4 until vms-fab; it is closed and owned none of them. - * vms-407 owns the missing arbitration, across rms_core.c, this file and - * rms_search.c. - * - * OVMX-USERSPACE: sys$get (vms-407) -- read(2) through the caller's own fd. - * OVMX-USERSPACE: sys$put (vms-407) -- write(2) through the caller's own fd. - * OVMX-USERSPACE: sys$update (vms-407) -- rewrite in place through that fd. - * OVMX-USERSPACE: sys$delete (vms-407) -- marks the record through that fd. - * OVMX-USERSPACE: sys$find (vms-407) -- positions that fd without transferring - * a record. + * OVMX-PARTIAL: sys$get (vms-bc7) -- exec: IO$_READVBLK reads the record's + * virtual block(s) through the ACP window (rms_io_read). + * OVMX-LOCAL: sys$get -- the RFM record framing / RAB cursor bookkeeping runs + * in this process; no executive record lock is taken. + * OVMX-PARTIAL: sys$put (vms-bc7) -- exec: IO$_WRITEVBLK writes the record's + * virtual block(s) through the ACP window (rms_io_write), extending on EOF. + * OVMX-LOCAL: sys$put -- the record framing / sequential-append positioning is + * this process's; no executive record lock is taken. + * OVMX-PARTIAL: sys$update (vms-bc7) -- exec: IO$_WRITEVBLK rewrites the record + * in place through the ACP window. + * OVMX-LOCAL: sys$update -- the in-process record framing decides what bytes to + * rewrite; no executive record lock is taken. + * OVMX-PARTIAL: sys$delete (vms-bc7) -- exec: IO$_WRITEVBLK marks the record's + * cell through the ACP window. + * OVMX-LOCAL: sys$delete -- the cell-status bookkeeping is this process's; no + * executive record lock is taken. + * OVMX-PARTIAL: sys$find (vms-bc7) -- exec: IO$_READVBLK reads the cell status + * through the ACP window to position without transferring a record. + * OVMX-LOCAL: sys$find -- the RAB positioning arithmetic is this process's; no + * executive record lock is taken. */ #include @@ -74,7 +87,7 @@ static struct FAB *validate_rab(struct RAB *rab) return NULL; } - if (fab->_linux_fd < 0) { + if (!fab->_rms_file) { rab->rab$l_sts = RMS$_ACC; return NULL; } diff --git a/src/vmsrms/rms_rel.c b/src/vmsrms/rms_rel.c index 6bece6c67..9b335b5d6 100644 --- a/src/vmsrms/rms_rel.c +++ b/src/vmsrms/rms_rel.c @@ -19,6 +19,7 @@ #include "rms/rms.h" #include "rms_internal.h" #include "rms_util.h" +#include "rms_io.h" /* Cell status byte values */ #define REL_CELL_EMPTY 0x00 /* Cell has never been used */ @@ -57,8 +58,8 @@ static size_t cell_size(struct FAB *fab) */ uint32_t rms_rel_get(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; if (!rab->rab$l_ubf || rab->rab$w_usz == 0) return RMS$_RAB; size_t csize = cell_size(fab); @@ -80,12 +81,12 @@ uint32_t rms_rel_get(struct FAB *fab, struct RAB *rab) /* Read loop: for sequential access, skip deleted/empty cells */ for (;;) { off_t offset = (off_t)(rrn * csize); - if (lseek(fd, offset, SEEK_SET) < 0) { + if (rms_io_lseek(fd, offset, SEEK_SET) < 0) { return RMS$_EOF; } uint8_t status_byte; - ssize_t n = read(fd, &status_byte, 1); + ssize_t n = rms_io_read(fd, &status_byte, 1); if (n <= 0) { return RMS$_EOF; } @@ -98,7 +99,7 @@ uint32_t rms_rel_get(struct FAB *fab, struct RAB *rab) return RMS$_RTB; } - n = rms_read_exact(fd, rab->rab$l_ubf, reclen); + n = rms_io_read_exact(fd, rab->rab$l_ubf, reclen); if (n < reclen) return RMS$_RER; rab->rab$w_rsz = reclen; @@ -143,8 +144,8 @@ uint32_t rms_rel_get(struct FAB *fab, struct RAB *rab) */ uint32_t rms_rel_put(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; char *buf = rab->rab$l_rbf ? rab->rab$l_rbf : rab->rab$l_ubf; if (!buf) return RMS$_RAB; @@ -162,11 +163,11 @@ uint32_t rms_rel_put(struct FAB *fab, struct RAB *rab) } off_t offset = (off_t)(rrn * csize); - if (lseek(fd, offset, SEEK_SET) < 0) return RMS$_RER; + if (rms_io_lseek(fd, offset, SEEK_SET) < 0) return RMS$_RER; /* Check if cell is already active */ uint8_t existing; - ssize_t n = read(fd, &existing, 1); + ssize_t n = rms_io_read(fd, &existing, 1); if (n == 1 && existing == REL_CELL_ACTIVE) { /* Cell already occupied - check UIF (update-if-existent) */ if (rab->rab$l_rop & RAB$M_UIF) { @@ -185,10 +186,10 @@ uint32_t rms_rel_put(struct FAB *fab, struct RAB *rab) } off_t offset = (off_t)(rrn * csize); - if (lseek(fd, offset, SEEK_SET) < 0) break; + if (rms_io_lseek(fd, offset, SEEK_SET) < 0) break; uint8_t status_byte; - ssize_t n = read(fd, &status_byte, 1); + ssize_t n = rms_io_read(fd, &status_byte, 1); if (n <= 0 || status_byte != REL_CELL_ACTIVE) { break; /* Found an empty or deleted cell */ } @@ -198,11 +199,11 @@ uint32_t rms_rel_put(struct FAB *fab, struct RAB *rab) /* Write the record at the determined position */ off_t offset = (off_t)(rrn * csize); - if (lseek(fd, offset, SEEK_SET) < 0) return RMS$_WER; + if (rms_io_lseek(fd, offset, SEEK_SET) < 0) return RMS$_WER; /* Write status byte */ uint8_t status_byte = REL_CELL_ACTIVE; - if (rms_write_exact(fd, &status_byte, 1) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, &status_byte, 1) < 0) return RMS$_WER; /* Write record data, padded to MRS */ uint16_t reclen = rab->rab$w_rsz; @@ -216,7 +217,7 @@ uint32_t rms_rel_put(struct FAB *fab, struct RAB *rab) memcpy(cell, buf, reclen); /* Remaining bytes stay zero (calloc) */ - int rc = rms_write_exact(fd, cell, fab->fab$w_mrs); + int rc = rms_io_write_exact(fd, cell, fab->fab$w_mrs); free(cell); if (rc < 0) return RMS$_WER; @@ -242,8 +243,8 @@ uint32_t rms_rel_put(struct FAB *fab, struct RAB *rab) */ uint32_t rms_rel_update(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; char *buf = rab->rab$l_rbf ? rab->rab$l_rbf : rab->rab$l_ubf; if (!buf) return RMS$_RAB; @@ -255,11 +256,11 @@ uint32_t rms_rel_update(struct FAB *fab, struct RAB *rab) size_t csize = cell_size(fab); off_t offset = (off_t)(rab->rab$l_bkt * csize); - if (lseek(fd, offset, SEEK_SET) < 0) return RMS$_RER; + if (rms_io_lseek(fd, offset, SEEK_SET) < 0) return RMS$_RER; /* Verify cell is still active */ uint8_t status_byte; - if (read(fd, &status_byte, 1) != 1 || status_byte != REL_CELL_ACTIVE) { + if (rms_io_read(fd, &status_byte, 1) != 1 || status_byte != REL_CELL_ACTIVE) { return RMS$_CUR; } @@ -274,7 +275,7 @@ uint32_t rms_rel_update(struct FAB *fab, struct RAB *rab) memcpy(cell, buf, reclen); - int rc = rms_write_exact(fd, cell, fab->fab$w_mrs); + int rc = rms_io_write_exact(fd, cell, fab->fab$w_mrs); free(cell); if (rc < 0) return RMS$_WER; @@ -294,8 +295,8 @@ uint32_t rms_rel_update(struct FAB *fab, struct RAB *rab) */ uint32_t rms_rel_delete(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; /* Must have a current record */ if (rab->_last_rec_offset == 0 && rab->rab$l_bkt == 0) { @@ -304,11 +305,11 @@ uint32_t rms_rel_delete(struct FAB *fab, struct RAB *rab) size_t csize = cell_size(fab); off_t offset = (off_t)(rab->rab$l_bkt * csize); - if (lseek(fd, offset, SEEK_SET) < 0) return RMS$_WER; + if (rms_io_lseek(fd, offset, SEEK_SET) < 0) return RMS$_WER; /* Mark cell as deleted */ uint8_t status_byte = REL_CELL_DELETED; - if (rms_write_exact(fd, &status_byte, 1) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, &status_byte, 1) < 0) return RMS$_WER; return RMS$_NORMAL; } @@ -327,8 +328,8 @@ uint32_t rms_rel_delete(struct FAB *fab, struct RAB *rab) */ uint32_t rms_rel_find(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; size_t csize = cell_size(fab); uint32_t rrn; @@ -344,10 +345,10 @@ uint32_t rms_rel_find(struct FAB *fab, struct RAB *rab) } off_t offset = (off_t)(rrn * csize); - if (lseek(fd, offset, SEEK_SET) < 0) return RMS$_RER; + if (rms_io_lseek(fd, offset, SEEK_SET) < 0) return RMS$_RER; uint8_t status_byte; - ssize_t n = read(fd, &status_byte, 1); + ssize_t n = rms_io_read(fd, &status_byte, 1); if (n <= 0) return RMS$_EOF; if (status_byte == REL_CELL_DELETED) return RMS$_DEL; diff --git a/src/vmsrms/rms_seq.c b/src/vmsrms/rms_seq.c index 106a96bdf..1ad93b8b0 100644 --- a/src/vmsrms/rms_seq.c +++ b/src/vmsrms/rms_seq.c @@ -18,6 +18,7 @@ #include "rms/rms.h" #include "rms_internal.h" #include "rms_util.h" +#include "rms_io.h" /* * rms_seq_get - Read a record from a sequential file. @@ -41,13 +42,13 @@ */ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; if (rab->_eof) return RMS$_EOF; if (!rab->rab$l_ubf || rab->rab$w_usz == 0) return RMS$_RAB; /* Seek to current stream position */ - lseek(fd, rab->_current_offset, SEEK_SET); + rms_io_lseek(fd, rab->_current_offset, SEEK_SET); /* Remember where this record starts */ rab->_last_rec_offset = rab->_current_offset; @@ -69,7 +70,7 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) ssize_t n; while (pos < rab->rab$w_usz) { - n = read(fd, &ch, 1); + n = rms_io_read(fd, &ch, 1); if (n <= 0) { if (pos == 0) { rab->_eof = 1; @@ -84,7 +85,7 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) if (ch == '\r' && fab->fab$b_rfm == FAB$C_STM) { /* Peek at next char */ char next; - ssize_t pn = read(fd, &next, 1); + ssize_t pn = rms_io_read(fd, &next, 1); if (pn > 0 && next == '\n') break; /* \r\n found */ /* Not \r\n, include \r in record */ if (pos < rab->rab$w_usz) rab->rab$l_ubf[pos++] = '\r'; @@ -98,7 +99,7 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) rab->rab$w_rsz = (uint16_t)pos; rab->_last_rec_size = (uint16_t)pos; - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); break; } @@ -114,7 +115,7 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) return RMS$_RTB; } - ssize_t n = rms_read_exact(fd, rab->rab$l_ubf, recsize); + ssize_t n = rms_io_read_exact(fd, rab->rab$l_ubf, recsize); if (n <= 0) { rab->_eof = 1; return RMS$_EOF; @@ -137,7 +138,7 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) * to word (2-byte) boundaries. */ uint16_t reclen; - ssize_t n = rms_read_exact(fd, &reclen, 2); + ssize_t n = rms_io_read_exact(fd, &reclen, 2); if (n <= 0) { rab->_eof = 1; return RMS$_EOF; @@ -147,22 +148,22 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) if (reclen > rab->rab$w_usz) { rab->rab$l_stv = reclen; /* Skip past the record data so we don't corrupt the stream */ - lseek(fd, reclen + (reclen & 1), SEEK_CUR); - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rms_io_lseek(fd, reclen + (reclen & 1), SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); return RMS$_RTB; } if (reclen > 0) { - n = rms_read_exact(fd, rab->rab$l_ubf, reclen); + n = rms_io_read_exact(fd, rab->rab$l_ubf, reclen); if (n < reclen) return RMS$_RER; } /* Skip pad byte for word alignment */ - if (reclen & 1) lseek(fd, 1, SEEK_CUR); + if (reclen & 1) rms_io_lseek(fd, 1, SEEK_CUR); rab->rab$w_rsz = reclen; rab->_last_rec_size = reclen; - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); break; } @@ -174,7 +175,7 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) * The count includes the fixed control area. */ uint16_t reclen; - ssize_t n = rms_read_exact(fd, &reclen, 2); + ssize_t n = rms_io_read_exact(fd, &reclen, 2); if (n <= 0) { rab->_eof = 1; return RMS$_EOF; @@ -188,8 +189,8 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) if (datalen > rab->rab$w_usz) { rab->rab$l_stv = datalen; - lseek(fd, reclen + (reclen & 1), SEEK_CUR); - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rms_io_lseek(fd, reclen + (reclen & 1), SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); return RMS$_RTB; } @@ -197,22 +198,22 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) if (fsz > 0) { char ctrl[256]; uint8_t to_read = (fsz <= sizeof(ctrl)) ? fsz : (uint8_t)sizeof(ctrl); - n = rms_read_exact(fd, ctrl, to_read); + n = rms_io_read_exact(fd, ctrl, to_read); if (n < to_read) return RMS$_RER; } /* Read the data portion */ if (datalen > 0) { - n = rms_read_exact(fd, rab->rab$l_ubf, datalen); + n = rms_io_read_exact(fd, rab->rab$l_ubf, datalen); if (n < datalen) return RMS$_RER; } /* Skip pad byte for word alignment */ - if (reclen & 1) lseek(fd, 1, SEEK_CUR); + if (reclen & 1) rms_io_lseek(fd, 1, SEEK_CUR); rab->rab$w_rsz = datalen; rab->_last_rec_size = datalen; - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); break; } @@ -240,41 +241,41 @@ uint32_t rms_seq_get(struct FAB *fab, struct RAB *rab) */ uint32_t rms_seq_put(struct FAB *fab, struct RAB *rab) { - int fd = fab->_linux_fd; - if (fd < 0) return RMS$_ACC; + struct rms_file *fd = fab->_rms_file; + if (!fd) return RMS$_ACC; char *buf = rab->rab$l_rbf ? rab->rab$l_rbf : rab->rab$l_ubf; if (!buf) return RMS$_RAB; uint16_t len = rab->rab$w_rsz; /* Seek to end for sequential writes */ - lseek(fd, 0, SEEK_END); + rms_io_lseek(fd, 0, SEEK_END); switch (fab->fab$b_rfm) { case FAB$C_STMLF: { /* Stream-LF: write data followed by \n */ if (len > 0) { - if (rms_write_exact(fd, buf, len) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, buf, len) < 0) return RMS$_WER; } - if (rms_write_exact(fd, "\n", 1) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, "\n", 1) < 0) return RMS$_WER; break; } case FAB$C_STMCR: { /* Stream-CR: write data followed by \r */ if (len > 0) { - if (rms_write_exact(fd, buf, len) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, buf, len) < 0) return RMS$_WER; } - if (rms_write_exact(fd, "\r", 1) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, "\r", 1) < 0) return RMS$_WER; break; } case FAB$C_STM: { /* Stream: write data followed by \r\n */ if (len > 0) { - if (rms_write_exact(fd, buf, len) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, buf, len) < 0) return RMS$_WER; } - if (rms_write_exact(fd, "\r\n", 2) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, "\r\n", 2) < 0) return RMS$_WER; break; } @@ -300,7 +301,7 @@ uint32_t rms_seq_put(struct FAB *fab, struct RAB *rab) uint16_t copylen = (len < recsize) ? len : recsize; memcpy(padded, buf, copylen); - int rc = rms_write_exact(fd, padded, recsize); + int rc = rms_io_write_exact(fd, padded, recsize); free(padded); if (rc < 0) return RMS$_WER; break; @@ -316,14 +317,14 @@ uint32_t rms_seq_put(struct FAB *fab, struct RAB *rab) return RMS$_RTB; } - if (rms_write_exact(fd, &len, 2) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, &len, 2) < 0) return RMS$_WER; if (len > 0) { - if (rms_write_exact(fd, buf, len) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, buf, len) < 0) return RMS$_WER; } /* Pad to word boundary */ if (len & 1) { char zero = 0; - if (rms_write_exact(fd, &zero, 1) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, &zero, 1) < 0) return RMS$_WER; } break; } @@ -343,22 +344,22 @@ uint32_t rms_seq_put(struct FAB *fab, struct RAB *rab) } uint16_t total = (uint16_t)(fsz + len); - if (rms_write_exact(fd, &total, 2) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, &total, 2) < 0) return RMS$_WER; /* Write fixed control area (zeroed) */ char ctrl[256]; memset(ctrl, 0, sizeof(ctrl)); - if (rms_write_exact(fd, ctrl, fsz) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, ctrl, fsz) < 0) return RMS$_WER; /* Write data */ if (len > 0) { - if (rms_write_exact(fd, buf, len) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, buf, len) < 0) return RMS$_WER; } /* Pad to word boundary */ if (total & 1) { char zero = 0; - if (rms_write_exact(fd, &zero, 1) < 0) return RMS$_WER; + if (rms_io_write_exact(fd, &zero, 1) < 0) return RMS$_WER; } break; } @@ -367,6 +368,6 @@ uint32_t rms_seq_put(struct FAB *fab, struct RAB *rab) return RMS$_ORG; } - rab->_current_offset = lseek(fd, 0, SEEK_CUR); + rab->_current_offset = rms_io_lseek(fd, 0, SEEK_CUR); return RMS$_NORMAL; } diff --git a/tests/qemu/facility_defects.sh b/tests/qemu/facility_defects.sh index 4f9a7f2ec..74e49ca33 100755 --- a/tests/qemu/facility_defects.sh +++ b/tests/qemu/facility_defects.sh @@ -497,6 +497,7 @@ acp-access-window-vbn-offbyone acp-writevb-extend-alloc-offbyone acp-search-cursor-skips-versions acp-create-header-slot-offbyone +rms-put-wrong-vbn p0-map-not-recorded p1-map-not-recorded p0-unmap-clears-p1 @@ -4868,6 +4869,23 @@ EOF why) echo "ods2_fh2_map_append() -- the pure codec helper that records a newly allocated extent in the file header's FM2 retrieval map -- encodes the extent's LBN as lbn+1 instead of lbn. The IO\$_WRITEVBLK extend path writes the caller's data to the LBN the allocator actually handed out and appends that SAME LBN to the in-memory channel window, so a read on the SAME channel (in-memory window) is correct; but the extent PERSISTED to the on-disk FH2 now points one block too high. After the file is DEACCESSed and re-ACCESSed, the window is rebuilt from the corrupted on-disk map, so IO\$_READVBLK of the extended VBN resolves to the wrong LBN and does not read the data back. This is exactly the property the implicit extend exists to provide -- that the grown allocation is durable, not a per-open illusion (INV-6) -- so only the re-ACCESS read-back assertion can tell the difference: the same-channel read (before deaccess) and the FH2 EOF/HIBLK assertions (set by ods2_fh2_set_eof, untouched) all stay green.";; require_fail) cat <<'EOF' after DEACCESS + re-ACCESS, the extended VBN 35 STILL reads its pattern (allocation persisted) +EOF + ;; + knock_on_fail) echo "";; + knock_on_why) echo "";; + esac;; + + rms-put-wrong-vbn) + case "$_f" in + facility) echo "RMS reaches file data through the Files-11 (ODS-2) ACP: a record \$PUT is IO\$_WRITEVBLK at the record's {VBN, byte-offset} on the file's channel window, and a \$GET is IO\$_READVBLK at the same coordinate (src/vmsrms/rms_io.c, the block-I/O substrate under rms_seq/rel/idx; RMS \$OPEN/\$CREATE/\$CLOSE ride the ACP in rms_core.c), vms-bc7, epic vms-208";; + targets) echo "vmsrms/rms_io.c";; + suites_red) echo "test_syssvc_rms_acp";; + blind_suites) echo "";; + blind_why) echo "";; + isolation) echo "isolated";; + why) echo "rms_io_write() -- the \$PUT side of the substrate -- computes the transfer's starting virtual block as (cursor / 512) + 2 instead of + 1, so every record RMS writes lands ONE VBN too high on disk (IO\$_WRITEVBLK persists it there, and grows the file's EOF to cover the gap). The \$GET side, rms_io_read(), is untouched and still computes the correct VBN, so after \$CLOSE + re-\$OPEN a record read back at its true VBN finds the empty (never-written) block instead of the data. This is exactly the property RMS-over-\$QIO exists to provide -- that a record written through the window is the record read back through it (INV-6: it hit the disk WHERE RMS says it did, not a block off) -- so the byte-exact readback assertions redden while \$CREATE/\$CONNECT/\$PUT (which still 'succeed', just to the wrong block) stay green. The read path being the untouched half is what lets the round-trip catch the write-side fault.";; + require_fail) cat <<'EOF' +RMS-over-ACP: all records round-tripped byte-exact through the ACP window EOF ;; knock_on_fail) echo "";; @@ -6665,6 +6683,17 @@ apply_edit() { # `((lbn + 1u) & 0xFFFF)`) -- the no-op the idempotency selftest requires. sed -i 's|w1 = (uint16_t)(lbn \& 0xFFFF);|w1 = (uint16_t)((lbn + 1u) \& 0xFFFF); /* NEGCTL acp-writevb-extend-alloc-offbyone */|' "$_file";; + rms-put-wrong-vbn) + # The `a.vbn = (cursor/512)+1u` line appears in BOTH rms_io_read and + # rms_io_write; scope the mutation to the WRITE function + # (/^ssize_t rms_io_write/,/^}$/) so ONLY the $PUT side is shifted -- the + # $GET side must stay CORRECT or the round-trip could not catch it (both + # shifted would be self-consistent and escape). `+ 1u` -> `+ 2u` writes + # every record ONE VBN too high; the untouched read then finds the empty + # true VBN on re-$OPEN. A second apply finds no `+ 1u;` left in the range + # and is the no-op the idempotency selftest requires. + sed -i '/^ssize_t rms_io_write/,/^}$/ s| a.vbn = (uint32_t)(f->cursor / RMS_IO_BLK) + 1u;| a.vbn = (uint32_t)(f->cursor / RMS_IO_BLK) + 2u; /* NEGCTL rms-put-wrong-vbn */|' "$_file";; + *) echo "facility_defects.sh: unknown defect '$_d'" >&2; return 2;; esac } diff --git a/tests/qemu/test_syssvc_rms_acp.c b/tests/qemu/test_syssvc_rms_acp.c new file mode 100644 index 000000000..85d71ebd0 --- /dev/null +++ b/tests/qemu/test_syssvc_rms_acp.c @@ -0,0 +1,258 @@ +/* + * test_syssvc_rms_acp.c - RMS reaches files through the Files-11 (ODS-2) ACP: + * $CREATE/$PUT/$GET/$CLOSE/$OPEN/$EXTEND/$ERASE ride channel + $QIO + * (IO$_ACCESS / IO$_CREATE / IO$_READVBLK / IO$_WRITEVBLK / IO$_MODIFY / + * IO$_DELETE) on a real /dev/vms, NOT a POSIX fd -- the RMS-over-$QIO rung of + * epic vms-208 (vms-bc7). + * + * WHAT THIS PROVES, through the public RMS system services (sys$create / + * sys$put / sys$get / sys$close / sys$open / sys$extend / sys$erase, + * src/vmsrms/rms_core.c + rms_seq.c + rms_io.c) against the real-VAX ODS-2 + * fixture the harness mounts WRITABLE on DKA0:: + * + * 1. SEQUENTIAL $CREATE + $PUT lands records ON DISK via IO$_WRITEVBLK. A + * sys$create of [OVMXDIR] assigns a real FID (IO$_CREATE from + * INDEXF.SYS), and each sys$put writes its record through the file's + * VBN->LBN window -- no _linux_fd, no POSIX write. + * 2. $CLOSE + re-$OPEN + $GET reads them back BYTE/RECORD-EXACT via + * IO$_READVBLK: the bytes IO$_WRITEVBLK put on the platter are exactly + * what a fresh IO$_ACCESS + read returns (INV-6: it hit the disk). + * 3. Proven for RFM=VAR, RFM=STMLF and RFM=FIX -- the record FRAMING logic + * (2-byte count prefix / LF delimiter / fixed size) is intact on top of + * the swapped block-I/O substrate. + * 4. $EXTEND grows the file's allocation (IO$_MODIFY) without moving EOF. + * 5. $ERASE deletes it (IO$_DELETE): a following sys$open is RMS$_FNF. + * + * NAME->FID VIA THE ACP. resolve_filename no longer calls vmsfs_to_linux_path; + * "DKA0:[OVMXDIR]" is resolved by $ASSIGNing DKA0: and walking the + * directory to a FID through IO$_ACCESS -- exercised here every open/create. + * + * NO /dev/vms -> honest SKIP (77), never a fake pass (Rule 9): RMS is now an + * executive-file consumer, so with no ACP there is nothing to assert. This is + * the ATOMIC-FLIP-GROUP behaviour: the plain userspace ctest (no /dev/vms) sees + * RMS fail-honest, and only this QEMU harness, with a real mounted SYS$DISK, + * proves the flip. + * + * ISOLATION. Every file is created under a UNIQUE name in [OVMXDIR] and ERASED + * before exit, so the net directory state is restored (only BITMAP/INDEXF bits + * cycle) -- the same discipline as test_syssvc_acp_create.c. + */ + +#include +#include +#include +#include + +#include "starlet.h" +#include "descrip.h" +#include "ssdef.h" +#include "vms_kif.h" +#include "rms/rms.h" + +#define EXIT_SKIP 77 +#define ODS2_UNIT "DKA0:" + +static int pass = 0; +static int fail = 0; + +static void check(int cond, const char *name) +{ + if (cond) { printf(" PASS: %s\n", name); pass++; } + else { printf(" FAIL: %s\n", name); fail++; } +} + +static int executive_present(void) +{ + int fd = vms_kif_open(); + if (fd < 0) + return 0; + vms_kif_close(); + return 1; +} + +/* One full RFM round-trip: create [OVMXDIR], $PUT `nrec` records, close, + * reopen, $GET them back and compare byte-exact, $EXTEND, then $ERASE and + * confirm the file is gone. Records are "REC:" so a stale read or a + * wrong record is caught. Returns nothing; asserts via check(). */ +static void rfm_roundtrip(const char *name, uint8_t rfm, uint16_t mrs) +{ + char spec[128]; + char recs[16][64]; + uint16_t reclen[16]; + const int nrec = 8; + struct FAB fab; + struct RAB rab; + uint32_t st; + char label[64]; + + snprintf(spec, sizeof(spec), "%s[OVMXDIR]%s", ODS2_UNIT, name); + + for (int i = 0; i < nrec; i++) { + snprintf(recs[i], sizeof(recs[i]), "REC%03d:%s", i, name); + reclen[i] = (uint16_t)strlen(recs[i]); + if (rfm == FAB$C_FIX) { + /* FIX records are exactly mrs bytes; sys$put space-pads a short + * record and sys$get returns the full mrs. Pad the source here so + * the byte-exact compare below is against the on-disk form. */ + while (reclen[i] < mrs && reclen[i] < sizeof(recs[i]) - 1) + recs[i][reclen[i]++] = ' '; + recs[i][reclen[i]] = '\0'; + } + } + + /* ---- $CREATE ---- */ + fab = cc$rms_fab; + fab.fab$l_fna = spec; + fab.fab$b_fns = (uint8_t)strlen(spec); + fab.fab$b_org = FAB$C_SEQ; + fab.fab$b_rfm = rfm; + fab.fab$b_rat = 0; + fab.fab$w_mrs = mrs; + fab.fab$b_fac = FAB$M_PUT | FAB$M_GET; + + st = sys$create(&fab, 0, 0); + snprintf(label, sizeof(label), "[%s] sys$create -> NORMAL", name); + check(st == RMS$_NORMAL, label); + snprintf(label, sizeof(label), "[%s] RMS file handle built (ACP window)", name); + check(fab._rms_file != 0, label); + if (st != RMS$_NORMAL) + return; + + /* ---- $CONNECT + $PUT loop ---- */ + rab = cc$rms_rab; + rab.rab$l_fab = &fab; + st = sys$connect(&rab, 0, 0); + snprintf(label, sizeof(label), "[%s] sys$connect -> NORMAL", name); + check(st == RMS$_NORMAL, label); + + int put_ok = 1; + for (int i = 0; i < nrec; i++) { + rab.rab$l_rbf = recs[i]; + rab.rab$w_rsz = reclen[i]; + st = sys$put(&rab, 0, 0); + if (st != RMS$_NORMAL) { put_ok = 0; break; } + } + snprintf(label, sizeof(label), "[%s] sys$put all records -> WRITEVBLK", name); + check(put_ok, label); + + st = sys$close(&fab, 0, 0); + snprintf(label, sizeof(label), "[%s] sys$close after create -> NORMAL", name); + check(st == RMS$_NORMAL, label); + + /* ---- re-$OPEN + $GET loop, byte-exact ---- */ + fab = cc$rms_fab; + fab.fab$l_fna = spec; + fab.fab$b_fns = (uint8_t)strlen(spec); + fab.fab$b_org = FAB$C_SEQ; + fab.fab$b_rfm = rfm; /* RFM supplied by the reader (FAT-persist deferred) */ + fab.fab$w_mrs = mrs; + fab.fab$b_fac = FAB$M_GET; + st = sys$open(&fab, 0, 0); + snprintf(label, sizeof(label), "[%s] sys$open (reopen) -> NORMAL", name); + check(st == RMS$_NORMAL, label); + if (st != RMS$_NORMAL) { sys$erase(&fab, 0, 0); return; } + + rab = cc$rms_rab; + rab.rab$l_fab = &fab; + char ubuf[64]; + rab.rab$l_ubf = ubuf; + rab.rab$w_usz = sizeof(ubuf); + st = sys$connect(&rab, 0, 0); + check(st == RMS$_NORMAL, " reopen sys$connect -> NORMAL"); + + int readback_ok = 1, got = 0; + for (int i = 0; i < nrec; i++) { + st = sys$get(&rab, 0, 0); + if (st != RMS$_NORMAL) { readback_ok = 0; break; } + got++; + if (rab.rab$w_rsz != reclen[i] || + memcmp(rab.rab$l_ubf, recs[i], reclen[i]) != 0) { + readback_ok = 0; + break; + } + } + snprintf(label, sizeof(label), + "[%s] $GET reads back %d records BYTE-EXACT (READVBLK)", name, nrec); + check(readback_ok && got == nrec, label); + + /* Next $GET is EOF. */ + st = sys$get(&rab, 0, 0); + snprintf(label, sizeof(label), "[%s] sys$get at EOF -> RMS$_EOF", name); + check(st == RMS$_EOF, label); + + sys$close(&fab, 0, 0); + + /* ---- $EXTEND (IO$_MODIFY): reopen for write, allocate, close ---- */ + fab = cc$rms_fab; + fab.fab$l_fna = spec; + fab.fab$b_fns = (uint8_t)strlen(spec); + fab.fab$b_org = FAB$C_SEQ; + fab.fab$b_rfm = rfm; + fab.fab$w_mrs = mrs; + fab.fab$b_fac = FAB$M_PUT | FAB$M_GET; + st = sys$open(&fab, 0, 0); + if (st == RMS$_NORMAL) { + fab.fab$l_alq = 4; /* allocate 4 more blocks */ + st = sys$extend(&fab, 0, 0); + snprintf(label, sizeof(label), "[%s] sys$extend (IO$_MODIFY) -> NORMAL", name); + check(st == RMS$_NORMAL, label); + sys$close(&fab, 0, 0); + } else { + snprintf(label, sizeof(label), "[%s] reopen-for-extend -> NORMAL", name); + check(0, label); + } + + /* ---- $ERASE (IO$_DELETE), then confirm gone ---- */ + fab = cc$rms_fab; + fab.fab$l_fna = spec; + fab.fab$b_fns = (uint8_t)strlen(spec); + st = sys$erase(&fab, 0, 0); + snprintf(label, sizeof(label), "[%s] sys$erase (IO$_DELETE) -> NORMAL", name); + check(st == RMS$_NORMAL, label); + + fab = cc$rms_fab; + fab.fab$l_fna = spec; + fab.fab$b_fns = (uint8_t)strlen(spec); + fab.fab$b_org = FAB$C_SEQ; + fab.fab$b_rfm = rfm; + fab.fab$b_fac = FAB$M_GET; + st = sys$open(&fab, 0, 0); + snprintf(label, sizeof(label), "[%s] sys$open after erase -> RMS$_FNF", name); + check(st == RMS$_FNF, label); + if (st == RMS$_NORMAL) sys$close(&fab, 0, 0); +} + +int main(void) +{ + uint32_t st; + + printf("=== test_syssvc_rms_acp (RMS $CREATE/$PUT/$GET/$CLOSE/$EXTEND/$ERASE " + "-> $QIO to the Files-11 ODS-2 ACP, vms-bc7) ===\n"); + + if (!executive_present()) { + printf(" SKIP: no /dev/vms -- RMS is an executive-file consumer; nothing " + "to assert without a real ACP (Rule 9).\n"); + return EXIT_SKIP; + } + + /* Mount the ODS-2 volume on DKA0: executive-global so $ASSIGN sees it. */ + st = vms_kif_acp_mount(ODS2_UNIT); /* idempotent */ + check($VMS_STATUS_SUCCESS(st), "DKA0: mounted executive-global for RMS"); + + rfm_roundtrip("RMSVAR.DAT", FAB$C_VAR, 0); + rfm_roundtrip("RMSSTM.DAT", FAB$C_STMLF, 0); + rfm_roundtrip("RMSFIX.DAT", FAB$C_FIX, 20); + + /* If rms_io_write() ($PUT) writes a record one VBN too high while + * rms_io_read() ($GET) still reads the true VBN, every byte-exact readback + * above misses on re-$OPEN -- so this whole-suite gate reddens exactly when + * the block-I/O substrate stops being byte-exact through the ACP window. */ + /* negctl: rms-put-wrong-vbn */ + check(fail == 0, + "RMS-over-ACP: all records round-tripped byte-exact through the ACP window"); + + vms_kif_acp_dmount(ODS2_UNIT); + + printf("=== RMS-over-ACP: %d passed, %d failed ===\n", pass, fail); + return fail ? 1 : 0; +} diff --git a/tests/qemu/test_syssvc_rms_scratch_create.c b/tests/qemu/test_syssvc_rms_scratch_create.c index c9cfd8446..c89e9d58e 100644 --- a/tests/qemu/test_syssvc_rms_scratch_create.c +++ b/tests/qemu/test_syssvc_rms_scratch_create.c @@ -159,7 +159,7 @@ static void init_fab(struct FAB *fab, struct XABKEY *xab, const char *filespec) fab->fab$l_fna = (char *)filespec; fab->fab$b_fns = (uint8_t)strlen(filespec); fab->fab$l_xab = xab; - fab->_linux_fd = -1; + fab->_rms_file = 0; /* vms-bc7: RMS handle (was _linux_fd) */ } static void init_rab(struct RAB *rab, struct FAB *fab) diff --git a/tests/vmsrms/test_vmsrms.c b/tests/vmsrms/test_vmsrms.c index 9bf995bcd..786219dd9 100644 --- a/tests/vmsrms/test_vmsrms.c +++ b/tests/vmsrms/test_vmsrms.c @@ -174,7 +174,7 @@ static void test_create_close(void) check(st == RMS$_NORMAL, "sys$create returns NORMAL"); check(fab.fab$l_sts == RMS$_CREATED, "fab$l_sts is RMS$_CREATED after create"); check(fab.fab$w_ifi != 0, "IFI assigned after create"); - check(fab._linux_fd >= 0, "linux fd is valid after create"); + check(fab._rms_file != 0, "RMS file handle is valid after create"); /* vms-bc7 */ /* * sys$create appends a version number (e.g. ;1) to the path. @@ -186,7 +186,7 @@ static void test_create_close(void) st = sys$close(&fab, 0, 0); check(st == RMS$_NORMAL, "sys$close returns NORMAL after create"); - check(fab._linux_fd == -1, "linux fd is -1 after close"); + check(fab._rms_file == 0, "RMS file handle cleared after close"); /* vms-bc7 */ check(fab.fab$w_ifi == 0, "IFI cleared after close"); /* Verify file exists on disk (use resolved path with version) */ From 0f0b60b26a4e91e07cba924d866c6b4270ea196a Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 17 Aug 2026 05:26:39 +0000 Subject: [PATCH 004/106] =?UTF-8?q?vms-481:=20DCL=20file=20commands=20+=20?= =?UTF-8?q?F$=20lexicals=20reach=20files=20via=20RMS/$QIO-ACP=20(ATOMIC-FL?= =?UTF-8?q?IP-GROUP=20=E2=80=94=20red-by-design)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DCL DIRECTORY / SET DEFAULT / MOUNT / TYPE / COPY / CREATE and the F$SEARCH / F$FILE_ATTRIBUTES / F$PARSE lexicals no longer reach files through vmsfs_to_linux_path() + POSIX opendir/stat/fopen on the /vms passthrough. They reach files the VMS way -- RMS ($OPEN/$GET/$CREATE/$PUT/$SEARCH) and an OVMX RMS attribute accessor -- which on the product runtime route to the Files-11 ODS-2 ACP over /dev/vms (epic vms-208), and on the netbsd-vax cross keep RMS's own POSIX backend until vms-d5d. RMS substrate (src/vmsrms): - rms_search.c: sys$search rerouted to the ACP wildcard directory context (IO$_ACPCONTROL) -- genuine ODS-2 order, real FIDs. Adds rms_search_fid() (DIRECTORY /FULL reads the real File ID) and rms_search_end(). This WIRES the previously-UNWIRED vms_kif_acp_acpcontrol to a product caller. - rms_core.c: adds rms_file_attr() -- the DIRECTORY /FULL + F$FILE_ATTRIBUTES source of truth: real FID + size + protection + dates + record format from the ODS-2 header via IO$_ACCESS's ATR list, not stat(). Shares rms_acp_resolve_did via rms_internal.h. DCL (src/vmsdcl): a new dcl_rms.h helper layer (homed in the existing dcl_filespec.c TU -- no new native-link TU, NOBJ stays 25) provides read/write/ dir/attr helpers over RMS. cmd_type/cmd_create/cmd_copy/cmd_directory, cmd_set_default, cmd_mount, and lex_search/lex_file_attributes/lex_parse route through them. DIRECTORY /FULL now emits the genuine ODS-2 File ID. cmd_mount mounts through the ACP ($MOUNT), WIRING the previously-UNWIRED vms_kif_acp_mount. vmsdcl now links vmsrms (Debug); mk_dcl.sh already --uses LIBVMSRMS$SHR. Fail-honest (Rule 9 / INV-6): no ACP-mounted SYS$DISK => the real RMS/SS$ error, never a silent POSIX fallback. PROVEN on a real /dev/vms (tests/qemu/test_syssvc_dcl_acp.c, QEMU kernel- executive harness, 20 passed / 0 failed): F$SEARCH/DIRECTORY returns A.TXT;3/;2/;1, B.TXT;1 in genuine ODS-2 order with real File IDs 14/13/12/16; rms_file_attr returns the same real FID + version + on-disk attributes; SET DEFAULT verifies a directory via the ACP; CREATE/TYPE/COPY round-trip byte-exact through the ACP; fail-honest edges. Negctl anchor dcl-acp-search-fid-fabricated (FLOOR-NO-BUMP). ATOMIC-FLIP-GROUP, red-by-design, DO NOT MERGE STANDALONE, stacks on #649 -> #644: existing DCL SYS$DISK tests now fail-honest with no boot-mounted ACP SYS$DISK (dcl-integration: %DCL-E-DIRECT / %RMS-E-FNF, no crashes). Co-lands with the flip that ACP-mounts SYS$DISK at boot. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/libvmssys/vms_kif.h | 17 +- src/vmsdcl/CMakeLists.txt | 7 +- src/vmsdcl/dcl_cmd_file.c | 705 ++++++++++++++++++++----------- src/vmsdcl/dcl_cmd_misc.c | 116 ++--- src/vmsdcl/dcl_cmd_set.c | 84 +++- src/vmsdcl/dcl_filespec.c | 273 ++++++++++++ src/vmsdcl/dcl_lexical.c | 349 +++++++-------- src/vmsdcl/include/dcl/dcl_rms.h | 84 ++++ src/vmsrms/include/rms/rms.h | 45 ++ src/vmsrms/rms_core.c | 94 ++++- src/vmsrms/rms_internal.h | 19 + src/vmsrms/rms_search.c | 426 ++++++++++++++----- tests/qemu/facility_defects.sh | 37 +- tests/qemu/test_syssvc_dcl_acp.c | 303 +++++++++++++ 14 files changed, 1893 insertions(+), 666 deletions(-) create mode 100644 src/vmsdcl/include/dcl/dcl_rms.h create mode 100644 tests/qemu/test_syssvc_dcl_acp.c diff --git a/src/libvmssys/vms_kif.h b/src/libvmssys/vms_kif.h index 47791cce5..e63e6af56 100644 --- a/src/libvmssys/vms_kif.h +++ b/src/libvmssys/vms_kif.h @@ -756,11 +756,10 @@ uint32_t vms_kif_bg_getsockopt(uint32_t exec_chan, int level, int optname, /* $MOUNT an ODS-2 volume into the executive-global mounted table by unit name * (e.g. "DKA0:"). Idempotent. SS$_NOSUCHDEV if /dev/vms is absent. - * OVMX-UNWIRED: vms_kif_acp_mount (vms-149) -- no product caller yet: PID 1's - * boot-time $MOUNT of the system disk is a later rung of epic vms-208 (the full - * $MOUNT that binds the backing device + validates the home block). Exercised - * now only by tests/qemu/test_syssvc_acp_channel.c, the same footing as - * vms_kif_get_resmaster. */ + * WIRED (vms-481, epic vms-208): the DCL MOUNT command mounts a volume through + * the ACP -- src/vmsdcl/dcl_cmd_misc.c cmd_mount emits vms_kif_acp_mount instead + * of the retired setuid mount(2) helper (the /vms passthrough is gone). Also + * exercised by tests/qemu/test_syssvc_acp_channel.c and test_syssvc_dcl_acp.c. */ uint32_t vms_kif_acp_mount(const char *devnam); /* $DISMOUNT: remove a volume from the executive-global mounted table. @@ -818,9 +817,11 @@ uint32_t vms_kif_acp_writevb(struct vms_acp_rw_args *args); * file-class channel and return the NEXT matching {name, version, FID}, or * SS$_NOMOREFILES when the context is exhausted. See src/kernel/vms_acp.h for * the FIB/wildcard interface. Returns SS$_NOSUCHDEV if /dev/vms is absent. - * OVMX-UNWIRED: vms_kif_acp_acpcontrol (vms-a0b) -- no product caller yet - * (RMS $SEARCH and the DCL DIRECTORY / F$SEARCH lexical wire to it in a later - * rung); exercised by tests/qemu/test_syssvc_acp_search.c against real /dev/vms. */ + * WIRED (vms-481, epic vms-208): RMS $SEARCH emits vms_kif_acp_acpcontrol -- + * src/vmsrms/rms_search.c drives the executive wildcard directory context, and + * the DCL DIRECTORY command + the F$SEARCH lexical reach it through sys$search + * (src/vmsdcl/dcl_filespec.c dcl_rms_dir_*). Also exercised by + * tests/qemu/test_syssvc_acp_search.c and test_syssvc_dcl_acp.c. */ uint32_t vms_kif_acp_acpcontrol(struct vms_acp_acpcontrol_args *args); /* diff --git a/src/vmsdcl/CMakeLists.txt b/src/vmsdcl/CMakeLists.txt index 250632bbe..0fe80f43f 100644 --- a/src/vmsdcl/CMakeLists.txt +++ b/src/vmsdcl/CMakeLists.txt @@ -138,8 +138,11 @@ endif() if(VMSDCL_STANDALONE) target_compile_options(vmsdcl PRIVATE -Wall -Wextra -O2) else() - # Core link libraries - target_link_libraries(vmsdcl PRIVATE pthread vms vmsfs vmsprocess vmslnm vmsqueue) + # Core link libraries. vms-481: DCL file commands + F$ file lexicals now + # reach files through RMS ($OPEN/$GET/$CREATE/$PUT/$SEARCH + rms_file_attr, + # which route to the Files-11 ODS-2 ACP), so vmsdcl links vmsrms. (The + # native-link build already --uses LIBVMSRMS$SHR; mk_dcl.sh.) + target_link_libraries(vmsdcl PRIVATE pthread vms vmsrms vmsfs vmsprocess vmslnm vmsqueue) # Try to find readline for better interactive experience (skip for static # builds; and for the netbsd-vax cross, which has no readline in its sysroot diff --git a/src/vmsdcl/dcl_cmd_file.c b/src/vmsdcl/dcl_cmd_file.c index 595340290..a62f275c6 100644 --- a/src/vmsdcl/dcl_cmd_file.c +++ b/src/vmsdcl/dcl_cmd_file.c @@ -20,6 +20,7 @@ #include "dcl/parser.h" #include "dcl/symbol.h" #include "dcl/dcl_cmd.h" +#include "dcl/dcl_rms.h" /* vms-481: file commands reach files via RMS/ACP */ #include "dcl/vms_messages.h" #include "ssdef.h" #include "stsdef.h" @@ -39,6 +40,17 @@ struct dir_entry { int has_btime; /* 1 = btime is a genuine creation time; 0 = the * backing volume records none, so DIRECTORY/FULL * must NOT invent one (INV-6). */ + /* vms-481: genuine ODS-2 header data when this entry came from the ACP + * (from_acp=1). The real File ID DIRECTORY /FULL emits, the on-disk + * protection/allocation, and the VMS 64-bit dates -- NOT stat()-derived. */ + int from_acp; /* 1 = fields below are genuine ACP/ODS-2 data */ + uint16_t fid_num, fid_seq; + uint8_t fid_rvn; + uint16_t vms_prot; /* ODS-2 file protection (4 nibbles S/O/G/W) */ + long alloc_blocks; /* highest allocated VBN (allocation quantity) */ + uint8_t credate[8]; /* VMS 64-bit creation time (0 => not recorded) */ + uint8_t revdate[8]; /* VMS 64-bit revision time */ + int has_cre, has_rev; }; /* /SIZE[=option] mode. VSI OpenVMS DCL Dictionary, DIRECTORY /SIZE: bare /SIZE @@ -122,6 +134,33 @@ static void dir_format_vmsdate(struct timespec ts, char *buf, size_t bufsize) tm.tm_hour, tm.tm_min, tm.tm_sec, cc); } +/* + * dir_format_vmsbintime - vms-481: format a VMS 64-bit absolute time (the ODS-2 + * ATR$C_CREDATE / _REVDATE the ACP returns) as "dd-MMM-yyyy hh:mm:ss.cc". + * + * A VMS binary time is 100-ns ticks since the Smithsonian base 17-NOV-1858 + * (public, documented -- clean-room, Rule 8); Unix time subtracts the + * 3506716800-second offset to 01-JAN-1970. Converted inline to a timespec and + * handed to dir_format_vmsdate, so no new cross-image RTL symbol (sys$numtim) + * is added to the DCL native-link graph (same reason dir_format_vmsdate formats + * inline). Zero (unset) => 0 and the caller prints an honest "not recorded". + */ +#define DIR_VMS_UNIX_OFFSET_SEC 3506716800LL +static int dir_format_vmsbintime(const uint8_t vt[8], char *buf, size_t bufsize) +{ + uint64_t ticks; + memcpy(&ticks, vt, 8); + if (ticks == 0) { snprintf(buf, bufsize, ""); return 0; } + long long secs = (long long)(ticks / 10000000ULL) - DIR_VMS_UNIX_OFFSET_SEC; + long nsec = (long)((ticks % 10000000ULL) * 100ULL); + if (secs < 0) { snprintf(buf, bufsize, ""); return 0; } + struct timespec ts; + ts.tv_sec = (time_t)secs; + ts.tv_nsec = nsec; + dir_format_vmsdate(ts, buf, bufsize); + return 1; +} + /* Per-category access bits within a VMS protection nibble (set = DENIED) and * the nibble shifts. Kept as literals (matching src/libvms/include/ * ovmx_fileprot.h) so no libvms header is pulled onto the DCL native-link @@ -376,6 +415,102 @@ static int dir_collect(const char *linux_dir, const char *pattern, return SS$_NORMAL; } +/* + * dir_collect_acp - vms-481: collect one directory's matching entries through + * the Files-11 ODS-2 ACP (sys$parse + sys$search wildcard directory context) + * instead of opendir()/readdir()/stat() on a /vms passthrough. Each entry + * carries the GENUINE File ID the search returned and the on-disk header + * attributes (size/allocation/protection/owner/dates) read via rms_file_attr + * -- so DIRECTORY /FULL emits a real File ID, verified against the codec, not a + * synthesized one (INV-6). `vms_pattern` is a VMS wildcard filespec (e.g. + * "DKA0:[DIR]*.*;*"). Fail-honest: no ACP-mounted SYS$DISK => 0 entries. + * + * Returns SS$_NORMAL with *out_entries (malloc'd; caller frees) and *out_count. + */ +static int dir_collect_acp(struct dcl_context *ctx, const char *vms_pattern, + char **excl_pats, int excl_count, + struct dir_entry **out_entries, int *out_count) +{ + *out_entries = NULL; + *out_count = 0; + + struct dcl_rms_dir *d = dcl_rms_dir_open(ctx, vms_pattern); + if (!d) return SS$_NOSUCHFILE; + + int capacity = 256; + struct dir_entry *entries = malloc((size_t)capacity * sizeof(*entries)); + if (!entries) { dcl_rms_dir_close(d); return SS$_INSFMEM; } + int entry_count = 0; + + char match[1024]; + uint16_t fnum, fseq; uint8_t frvn; + while (dcl_rms_dir_next(d, match, sizeof(match), &fnum, &fseq, &frvn)) { + /* Resultant is "DEV:[DIR]NAME.TYP;VER" -- take the NAME.TYP;VER tail. */ + const char *nt = match; + const char *rb = strrchr(match, ']'); + if (!rb) rb = strrchr(match, '>'); + if (rb) nt = rb + 1; + + /* /EXCLUDE: skip entries matching any exclusion spec (same VMS matcher). */ + if (excl_count > 0) { + int excluded = 0; + for (int xi = 0; xi < excl_count; xi++) { + if (vmsfs_wildcard_match(excl_pats[xi], nt)) { excluded = 1; break; } + } + if (excluded) continue; + } + + if (entry_count >= capacity) { + capacity *= 2; + struct dir_entry *tmp = realloc(entries, + (size_t)capacity * sizeof(*entries)); + if (!tmp) { free(entries); dcl_rms_dir_close(d); return SS$_INSFMEM; } + entries = tmp; + } + + struct dir_entry *e = &entries[entry_count]; + memset(e, 0, sizeof(*e)); + e->from_acp = 1; + e->fid_num = fnum; e->fid_seq = fseq; e->fid_rvn = frvn; + strncpy(e->vms_name, nt, sizeof(e->vms_name) - 1); + e->vms_name[sizeof(e->vms_name) - 1] = '\0'; + strncpy(e->raw_name, nt, sizeof(e->raw_name) - 1); + e->raw_name[sizeof(e->raw_name) - 1] = '\0'; + char *semi = strrchr(e->vms_name, ';'); + e->version = (semi && semi[1]) ? (int)strtol(semi + 1, NULL, 10) : 1; + + /* Genuine ODS-2 header attributes (real File ID we already have). */ + struct rms_fileattr at; + if (rms_file_attr(match, &at) == RMS$_NORMAL) { + long used = at.efblk ? (at.ffbyte ? (long)at.efblk + : (long)at.efblk - 1) : 0; + if (used < 0) used = 0; + e->blocks = used; + e->alloc_blocks = (long)at.hiblk; + e->vms_prot = at.fileprot; + e->st.st_size = (off_t)used * 512 + at.ffbyte; + e->st.st_blocks = (blkcnt_t)at.hiblk; + e->st.st_gid = at.uic_group; + e->st.st_uid = at.uic_member; + e->st.st_mode = at.is_directory ? (S_IFDIR | 0755) : (S_IFREG | 0644); + memcpy(e->credate, at.credate, 8); + memcpy(e->revdate, at.revdate, 8); + e->has_cre = (memcmp(at.credate, "\0\0\0\0\0\0\0\0", 8) != 0); + e->has_rev = (memcmp(at.revdate, "\0\0\0\0\0\0\0\0", 8) != 0); + } else { + e->st.st_mode = S_IFREG | 0644; + } + + entry_count++; + } + dcl_rms_dir_close(d); + + qsort(entries, (size_t)entry_count, sizeof(struct dir_entry), dir_entry_cmp); + *out_entries = entries; + *out_count = entry_count; + return SS$_NORMAL; +} + /* * dir_print_entries - Print the file listing for one already-collected, * already-sorted directory, honoring the display qualifiers, and return the @@ -431,11 +566,17 @@ static void dir_print_entries(const struct dir_entry *entries, int entry_count, * organization, record format/attributes, longest-record length) * are OMITTED rather than fabricated (INV-6 / vms-5eb) -- see the * vms-5e2 PR's source-and-gap table. */ - long alloc = (long)st->st_blocks; /* real on-disk allocation */ - - /* Line 1: file name. (Real VMS also prints "File ID: (n,n,n)" here; - * omitted -- no genuine File ID exists for a passthrough file.) */ - printf("%s\n", vms_name); + long alloc = e->from_acp ? e->alloc_blocks + : (long)st->st_blocks; /* on-disk allocation */ + + /* Line 1: file name, and -- when the entry came from the ACP -- the + * GENUINE ODS-2 File ID (num,seq,rvn) from the directory search, the + * fidelity gap the /vms passthrough could not fill (vms-481). */ + if (e->from_acp) + printf("%-30s File ID: (%u,%u,%u)\n", + vms_name, e->fid_num, e->fid_seq, e->fid_rvn); + else + printf("%s\n", vms_name); /* Size (used/allocated) + Owner UIC [group,member]. */ char sizebuf[32]; @@ -445,27 +586,41 @@ static void dir_print_entries(const struct dir_entry *entries, int entry_count, (unsigned)(st->st_gid & 0377), (unsigned)(st->st_uid & 0377)); - /* Created: genuine birth time, or an honest gap marker when the - * backing volume records none. Revised: real mtime. Both carry the - * real .cc fraction (the vms-5e2 fidelity fix vs the old ".00"). */ + /* Created / Revised. From the ACP these are the file header's real + * ODS-2 ATR$C_CREDATE / _REVDATE; from the passthrough they are the + * birth time (or an honest gap) and the real mtime. */ char datebuf[40]; - if (e->has_btime) { - dir_format_vmsdate(e->btime, datebuf, sizeof(datebuf)); - printf("Created: %s\n", datebuf); + if (e->from_acp) { + if (e->has_cre && dir_format_vmsbintime(e->credate, datebuf, + sizeof(datebuf))) + printf("Created: %s\n", datebuf); + else + printf("Created: \n"); + if (e->has_rev && dir_format_vmsbintime(e->revdate, datebuf, + sizeof(datebuf))) + printf("Revised: %s\n", datebuf); + else + printf("Revised: \n"); } else { - printf("Created: \n"); + if (e->has_btime) { + dir_format_vmsdate(e->btime, datebuf, sizeof(datebuf)); + printf("Created: %s\n", datebuf); + } else { + printf("Created: \n"); + } + dir_format_vmsdate(st->st_mtim, datebuf, sizeof(datebuf)); + printf("Revised: %s\n", datebuf); } - dir_format_vmsdate(st->st_mtim, datebuf, sizeof(datebuf)); - printf("Revised: %s\n", datebuf); - /* Expiration/backup dates are genuinely unset for a passthrough - * file -- VMS prints exactly these strings for a file that has - * none, so this is faithful, not invented. */ + /* Expiration/backup dates are genuinely unset here -- VMS prints + * exactly these strings for a file that has none. */ printf("Expired: \n"); printf("Backup: \n"); - /* File protection (long form) from the real st_mode. */ - uint16_t vprot = vmsfs_mode_to_protection(st->st_mode); + /* File protection (long form): the genuine ODS-2 protection word + * from the ACP header, else derived from the passthrough st_mode. */ + uint16_t vprot = e->from_acp ? e->vms_prot + : vmsfs_mode_to_protection(st->st_mode); char protbuf[80]; dir_format_prot_full(vprot, protbuf, sizeof(protbuf)); printf("File protection: %s\n", protbuf); @@ -489,9 +644,14 @@ static void dir_print_entries(const struct dir_entry *entries, int entry_count, } } if (o->show_date) { - /* Real .cc fraction from the mtime, not a hardcoded ".00". */ char datebuf[40]; - dir_format_vmsdate(st->st_mtim, datebuf, sizeof(datebuf)); + if (e->from_acp) { + if (!(e->has_rev && dir_format_vmsbintime(e->revdate, datebuf, + sizeof(datebuf)))) + snprintf(datebuf, sizeof(datebuf), ""); + } else { + dir_format_vmsdate(st->st_mtim, datebuf, sizeof(datebuf)); + } printf(" %s", datebuf); } if (o->show_owner) { @@ -500,7 +660,8 @@ static void dir_print_entries(const struct dir_entry *entries, int entry_count, (unsigned)(st->st_uid & 0377)); } if (o->show_protection) { - uint16_t vprot = vmsfs_mode_to_protection(st->st_mode); + uint16_t vprot = e->from_acp ? e->vms_prot + : vmsfs_mode_to_protection(st->st_mode); char prot_buf[64]; vmsfs_format_protection(vprot, prot_buf, sizeof(prot_buf)); printf(" %s", prot_buf); @@ -589,6 +750,78 @@ static void dir_gather_recurse(const char *d, char ***list, int *count, int *cap free(subs); } +/* + * dir_spec_child - vms-481: build the VMS directory spec of a subdirectory. + * base is "DEV:[DIR]" (or "DEV:[000000]" for the MFD); sub is the subdirectory + * NAME (from a "NAME.DIR" entry). Returns "DEV:[DIR.SUB]" (or "DEV:[SUB]" from + * the MFD). Returns 0 on success, -1 if base has no bracketed directory. + */ +static int dir_spec_child(const char *base, const char *sub, + char *out, size_t outsz) +{ + const char *lb = strchr(base, '['); + const char *rb = lb ? strchr(lb, ']') : NULL; + if (!lb || !rb || rb <= lb) return -1; + char prefix[256]; + size_t pl = (size_t)(lb - base); + if (pl >= sizeof(prefix)) pl = sizeof(prefix) - 1; + memcpy(prefix, base, pl); prefix[pl] = '\0'; + char dir[512]; + size_t dl = (size_t)(rb - lb - 1); + if (dl >= sizeof(dir)) dl = sizeof(dir) - 1; + memcpy(dir, lb + 1, dl); dir[dl] = '\0'; + if (dir[0] == '\0' || strcmp(dir, "000000") == 0) + snprintf(out, outsz, "%s[%s]", prefix, sub); + else + snprintf(out, outsz, "%s[%s.%s]", prefix, dir, sub); + return 0; +} + +/* + * dir_gather_acp - vms-481: depth-first collect a VMS directory spec plus every + * subdirectory below it, through the ACP wildcard search for "*.DIR" (the + * genuine ODS-2 directory tree, not opendir). This is the ellipsis "[...]" walk + * done the VMS way. Appends each directory spec (malloc'd) to *list. + */ +static void dir_gather_acp(struct dcl_context *ctx, const char *dirspec, + char ***list, int *count, int *cap, int depth) +{ + if (depth > 32) return; /* guard against a pathological / cyclic tree */ + + if (*count >= *cap) { + int nc = *cap ? *cap * 2 : 16; + char **tmp = realloc(*list, (size_t)nc * sizeof(char *)); + if (!tmp) return; + *list = tmp; *cap = nc; + } + (*list)[(*count)++] = strdup(dirspec); + + char subpat[1024]; + snprintf(subpat, sizeof(subpat), "%s*.DIR;*", dirspec); + struct dcl_rms_dir *d = dcl_rms_dir_open(ctx, subpat); + if (!d) return; + + char match[1024]; + while (dcl_rms_dir_next(d, match, sizeof(match), NULL, NULL, NULL)) { + const char *nt = match; + const char *rb = strrchr(match, ']'); + if (!rb) rb = strrchr(match, '>'); + if (rb) nt = rb + 1; + /* nt is "NAME.DIR;VER" -- take NAME. */ + char sub[256]; + size_t i = 0; + for (; nt[i] && nt[i] != '.' && i < sizeof(sub) - 1; i++) sub[i] = nt[i]; + sub[i] = '\0'; + if (sub[0] == '\0') continue; + /* Skip the MFD self-reference 000000.DIR. */ + if (strcasecmp(sub, "000000") == 0) continue; + char child[1024]; + if (dir_spec_child(dirspec, sub, child, sizeof(child)) == 0) + dir_gather_acp(ctx, child, list, count, cap, depth + 1); + } + dcl_rms_dir_close(d); +} + /* * dir_deellipsize - Rewrite a VMS directory spec that contains the "..." * ellipsis wildcard into a plain, resolvable directory spec naming the START @@ -649,15 +882,15 @@ int cmd_directory(struct dcl_command *cmd) { struct dcl_context *ctx = dcl_get_context(); - /* Determine the directory to list */ - char linux_dir[1024]; - const char *pattern = NULL; + /* vms-481: DIRECTORY lists through the Files-11 ODS-2 ACP (sys$parse + + * sys$search wildcard directory context + rms_file_attr), emitting the + * genuine File ID and on-disk attributes -- not opendir()/stat() on a /vms + * passthrough. The listing is resolved as a VMS filespec throughout. */ /* Detect and strip a "..." ellipsis directory wildcard. `use_spec` is the - * spec with the ellipsis removed (naming the START of the tree); the - * resolution below treats it exactly like a non-ellipsis spec, and when + * spec with the ellipsis removed (naming the START of the tree); when * has_ellipsis is set the listing walks the resolved directory's whole - * subtree (see dir_deellipsize() / dir_gather_recurse()). */ + * subtree via the ACP (dir_gather_acp). */ char despec[1024]; int has_ellipsis = 0; const char *use_spec = NULL; @@ -666,37 +899,41 @@ int cmd_directory(struct dcl_command *cmd) use_spec = despec; } - if (use_spec && use_spec[0] != '\0') { - dcl_resolve_path(ctx, use_spec, linux_dir, sizeof(linux_dir)); - /* Check if this is a directory or a file pattern */ - struct stat st; - if (stat(linux_dir, &st) == 0 && S_ISDIR(st.st_mode)) { - /* It's a directory */ - } else { - /* Might be a wildcard pattern - split dir and pattern. - * Use the ORIGINAL filename+version text, not linux_dir's own - * basename — see dcl_filename_component()'s doc comment. */ - const char *orig = dcl_filename_component(use_spec); - char *last_slash = strrchr(linux_dir, '/'); - if (last_slash) { - pattern = strdup((orig && orig[0]) ? orig : last_slash + 1); - *(last_slash + 1) = '\0'; - } else { - pattern = strdup((orig && orig[0]) ? orig : linux_dir); - vmsfs_to_linux_path(ctx->default_dir, linux_dir, sizeof(linux_dir)); - } - } - } else { - vmsfs_to_linux_path(ctx->default_dir, linux_dir, sizeof(linux_dir)); + /* The full VMS wildcard filespec the ACP search iterates. A spec ending in + * a directory/device terminator (']' '>' ':') lists all files there; an + * empty spec lists the process default directory. */ + char vms_pattern[1024]; + if (use_spec && use_spec[0] != '\0') + dcl_rms_effective_spec(ctx, use_spec, vms_pattern, sizeof(vms_pattern)); + else + dcl_rms_effective_spec(ctx, "*.*;*", vms_pattern, sizeof(vms_pattern)); + { + size_t L = strlen(vms_pattern); + char last = L ? vms_pattern[L - 1] : 0; + if (last == ']' || last == '>' || last == ':') + strncat(vms_pattern, "*.*;*", sizeof(vms_pattern) - L - 1); } - /* Ensure trailing slash */ - size_t dlen = strlen(linux_dir); - if (dlen > 0 && linux_dir[dlen - 1] != '/') { - if (dlen < sizeof(linux_dir) - 1) { - linux_dir[dlen] = '/'; - linux_dir[dlen + 1] = '\0'; + /* Split the effective pattern into a directory-spec prefix and a file + * pattern (used by the ellipsis subtree walk to re-anchor per directory). */ + char vms_base[1024], vms_filepat[256]; + { + const char *rb = strrchr(vms_pattern, ']'); + if (!rb) rb = strrchr(vms_pattern, '>'); + if (!rb) { const char *cn = strrchr(vms_pattern, ':'); if (cn) rb = cn; } + if (rb) { + size_t pl = (size_t)(rb - vms_pattern) + 1; + if (pl >= sizeof(vms_base)) pl = sizeof(vms_base) - 1; + memcpy(vms_base, vms_pattern, pl); vms_base[pl] = '\0'; + strncpy(vms_filepat, rb + 1, sizeof(vms_filepat) - 1); + vms_filepat[sizeof(vms_filepat) - 1] = '\0'; + } else { + vms_base[0] = '\0'; + strncpy(vms_filepat, vms_pattern, sizeof(vms_filepat) - 1); + vms_filepat[sizeof(vms_filepat) - 1] = '\0'; } + if (vms_filepat[0] == '\0') + strncpy(vms_filepat, "*.*;*", sizeof(vms_filepat) - 1); } /* Check qualifiers */ @@ -812,15 +1049,13 @@ int cmd_directory(struct dcl_command *cmd) /* ---------------- Single-directory listing ---------------- */ struct dir_entry *entries = NULL; int entry_count = 0; - int cst = dir_collect(linux_dir, pattern, excl_pats, excl_count, - &entries, &entry_count); + int cst = dir_collect_acp(ctx, vms_pattern, excl_pats, excl_count, + &entries, &entry_count); if (cst == SS$_NOSUCHFILE) { - dcl_error("RMS", 2, "DNF", "directory not found - %s", linux_dir); - if (pattern) free((void *)pattern); + dcl_error("RMS", 2, "DNF", "directory not found - %s", vms_pattern); return SS$_NOSUCHFILE; } if (cst != SS$_NORMAL) { - if (pattern) free((void *)pattern); return cst; } @@ -831,7 +1066,6 @@ int cmd_directory(struct dcl_command *cmd) * "%DIRECT-W-NOFILES, no files found". */ if (entry_count == 0) { free(entries); - if (pattern) free((void *)pattern); dcl_error("DIRECT", STS$K_WARNING, "NOFILES", "no files found"); return SS$_NOSUCHFILE; } @@ -879,29 +1113,27 @@ int cmd_directory(struct dcl_command *cmd) printf("%s\n", vms_dir); } - if (pattern) free((void *)pattern); return SS$_NORMAL; } /* ---------------- Multi-directory ellipsis listing ---------------- * The "..." wildcard names the start directory plus every subdirectory - * below it. Walk the real on-disk tree, list each directory that has at - * least one match with its own header + "Total of N files" subtotal, then - * emit a single "Grand total of D directories, F files[, M blocks]." line. + * below it. Walk the genuine ODS-2 directory tree through the ACP + * (dir_gather_acp: "*.DIR" search per level), list each directory that has + * at least one match with its own header + "Total of N files" subtotal, + * then emit a single "Grand total of D directories, F files[, M blocks]." * Grounded (clean-room, Rule 8): VSI OpenVMS DCL Dictionary, DIRECTORY — * ellipsis directory wildcard + the per-directory / Grand total layout. */ - { - struct stat bst; - if (stat(linux_dir, &bst) != 0 || !S_ISDIR(bst.st_mode)) { - dcl_error("RMS", 2, "DNF", "directory not found - %s", linux_dir); - if (pattern) free((void *)pattern); - return SS$_NOSUCHFILE; - } - } + char ell_base[1024]; + if (vms_base[0]) + strncpy(ell_base, vms_base, sizeof(ell_base) - 1); + else + dcl_rms_effective_spec(ctx, "", ell_base, sizeof(ell_base)); /* default dir */ + ell_base[sizeof(ell_base) - 1] = '\0'; char **dirs = NULL; int ndirs = 0, dcap = 0; - dir_gather_recurse(linux_dir, &dirs, &ndirs, &dcap); + dir_gather_acp(ctx, ell_base, &dirs, &ndirs, &dcap, 0); long grand_used = 0, grand_alloc = 0, grand_files = 0; int grand_dirs = 0; @@ -909,18 +1141,15 @@ int cmd_directory(struct dcl_command *cmd) for (int di = 0; di < ndirs; di++) { struct dir_entry *entries = NULL; int entry_count = 0; - int cst = dir_collect(dirs[di], pattern, excl_pats, excl_count, - &entries, &entry_count); + char dpat[1200]; + snprintf(dpat, sizeof(dpat), "%s%s", dirs[di], vms_filepat); + int cst = dir_collect_acp(ctx, dpat, excl_pats, excl_count, + &entries, &entry_count); if (cst != SS$_NORMAL) { free(entries); continue; } if (entry_count == 0) { free(entries); continue; } - char display_dir[1024]; - strncpy(display_dir, dirs[di], sizeof(display_dir) - 1); - display_dir[sizeof(display_dir) - 1] = '\0'; - size_t ddlen = strlen(display_dir); - if (ddlen > 1 && display_dir[ddlen - 1] == '/') - display_dir[ddlen - 1] = '\0'; - dcl_format_directory(display_dir, vms_dir, sizeof(vms_dir)); + strncpy(vms_dir, dirs[di], sizeof(vms_dir) - 1); + vms_dir[sizeof(vms_dir) - 1] = '\0'; int file_count = 0; long used = 0, alloc = 0; @@ -950,7 +1179,6 @@ int cmd_directory(struct dcl_command *cmd) free(dirs); if (grand_files == 0) { - if (pattern) free((void *)pattern); dcl_error("DIRECT", STS$K_WARNING, "NOFILES", "no files found"); return SS$_NOSUCHFILE; } @@ -967,7 +1195,6 @@ int cmd_directory(struct dcl_command *cmd) printf(", %ld block%s", grand_used, grand_used != 1 ? "s" : ""); printf(".\n"); - if (pattern) free((void *)pattern); return SS$_NORMAL; } @@ -983,11 +1210,13 @@ int cmd_type(struct dcl_command *cmd) return SS$_BADPARAM; } - char linux_path[1024]; - dcl_resolve_path(ctx, cmd->params[0], linux_path, sizeof(linux_path)); - - FILE *fp = fopen(linux_path, "r"); - if (!fp) { + /* vms-481: TYPE reads the file through RMS ($OPEN + sequential $GET), which + * on the product runtime reaches the Files-11 ODS-2 ACP over /dev/vms -- not + * fopen() on a vmsfs_to_linux_path("/vms/...") path. Fail-honest: no + * ACP-mounted SYS$DISK => RMS$_FNF/ACC, never a silent POSIX read (INV-6). */ + uint32_t rst = 0; + struct dcl_rms_reader *r = dcl_rms_read_open(ctx, cmd->params[0], &rst); + if (!r) { dcl_error("RMS", 2, "FNF", "file not found - %s", cmd->params[0]); return SS$_NOSUCHFILE; @@ -999,8 +1228,10 @@ int cmd_type(struct dcl_command *cmd) int page_size = 24; char line[4096]; - while (fgets(line, sizeof(line), fp)) { - fputs(line, stdout); + int eof = 0, len; + while ((len = dcl_rms_read_record(r, line, sizeof(line), &eof)) >= 0) { + fwrite(line, 1, (size_t)len, stdout); + fputc('\n', stdout); /* RMS records carry no terminator */ line_count++; if (paged && line_count >= page_size) { @@ -1012,7 +1243,7 @@ int cmd_type(struct dcl_command *cmd) } } - fclose(fp); + dcl_rms_read_close(r); return SS$_NORMAL; } @@ -1255,25 +1486,50 @@ static void map_out_field(const char *out_field, const char *in_field, } /* - * copy_one - Byte-copy src_full to dst_full on the backing store. - * Returns SS$_NORMAL, or a VMS error (message already emitted by caller path). + * copy_one_rms - vms-481: copy src_spec to dst_spec through RMS, record by + * record. Source is read via $OPEN + sequential $GET; the destination is minted + * via $CREATE (which, on the product runtime, allocates a real FID from + * INDEXF.SYS and enters a new highest version through the Files-11 ODS-2 ACP) + * and written via sequential $PUT. The destination inherits the source's record + * format/attributes (rfm/rat/mrs) from its ODS-2 header (rms_file_attr) so a + * copied file keeps its record structure -- not a raw byte stream. No fopen on a + * /vms passthrough; fail-honest on an absent ACP (INV-6). + * + * ctx is used only for device/directory defaulting; src_spec and dst_spec are + * VMS filespecs. Returns SS$_NORMAL, or a VMS error. */ -static int copy_one(const char *src_full, const char *dst_full) +static int copy_one_rms(struct dcl_context *ctx, + const char *src_spec, const char *dst_spec) { - FILE *src = fopen(src_full, "rb"); - if (!src) return SS$_NOSUCHFILE; - FILE *dst = fopen(dst_full, "wb"); - if (!dst) { fclose(src); return SS$_FILACCERR; } + struct rms_fileattr sattr; + uint8_t rfm = FAB$C_VAR, rat = 0; + uint16_t mrs = 0; - char buf[8192]; - size_t nr; - int write_err = 0; - while ((nr = fread(buf, 1, sizeof(buf), src)) > 0) { - if (fwrite(buf, 1, nr, dst) != nr) { write_err = 1; break; } + /* Inherit the source record format where we can read it (genuine ODS-2 + * header via the ACP); default to variable-length text otherwise. */ + if (dcl_rms_attr(ctx, src_spec, &sattr) == RMS$_NORMAL && sattr.rfm) { + rfm = sattr.rfm; + rat = (uint8_t)sattr.rat; + mrs = sattr.mrs; } - fclose(src); - fclose(dst); - return write_err ? SS$_ABORT : SS$_NORMAL; + + uint32_t rst = 0; + struct dcl_rms_reader *r = dcl_rms_read_open(ctx, src_spec, &rst); + if (!r) return SS$_NOSUCHFILE; + + struct dcl_rms_writer *w = dcl_rms_write_create(ctx, dst_spec, rfm, rat, mrs, &rst); + if (!w) { dcl_rms_read_close(r); return SS$_FILACCERR; } + + static char rec[65536]; + int eof = 0, len, rc = SS$_NORMAL; + while ((len = dcl_rms_read_record(r, rec, sizeof(rec), &eof)) >= 0) { + if (dcl_rms_write_record(w, rec, (size_t)len) != 0) { rc = SS$_ABORT; break; } + } + if (!eof && rc == SS$_NORMAL && len < 0) rc = SS$_ABORT; /* hard read error */ + + dcl_rms_read_close(r); + dcl_rms_write_close(w); + return rc; } /* @@ -1345,124 +1601,116 @@ int cmd_copy(struct dcl_command *cmd) int do_confirm = dcl_has_qualifier(cmd, "CONFIRM"); int new_version = dcl_has_qualifier(cmd, "NEW_VERSION"); - /* Source: directory + name.type pattern + version spec. */ - char src_dir[1024], src_pat[512], src_vspec[64]; - int src_hasver; - split_file_spec(ctx, cmd->params[0], src_dir, sizeof(src_dir), - src_pat, sizeof(src_pat), - src_vspec, sizeof(src_vspec), &src_hasver); - - /* Destination: an existing directory takes name/type from each source - * (fields "*"); otherwise parse the destination name.type;ver fields. */ - char dst_dir[1024]; - char dname_pat[256] = "*", dtype_pat[256] = "*", dst_vspec[64] = ""; - int dst_hasver = 0; + (void)new_version; /* RMS $CREATE mints a new highest version by default */ - char dst_resolved[1024]; - dcl_resolve_path(ctx, cmd->params[1], dst_resolved, sizeof(dst_resolved)); - struct stat dst_st; - int dst_is_dir = (stat(dst_resolved, &dst_st) == 0 && S_ISDIR(dst_st.st_mode)); + /* + * vms-481: COPY reaches files through RMS/$QIO-ACP. The source is enumerated + * by the executive wildcard search (sys$parse + sys$search over the ODS-2 + * ACP -- genuine directory order, real FIDs), each file is copied record by + * record via RMS $GET/$PUT, and the destination is minted by $CREATE, which + * allocates a real FID and enters a NEW HIGHEST VERSION through the ACP (the + * VMS never-overwrite guarantee, now the executive's, not an opendir scan). + * No opendir/fopen on a /vms passthrough; fail-honest on an absent ACP. + */ - if (dst_is_dir) { - size_t dl = strlen(dst_resolved); - strncpy(dst_dir, dst_resolved, sizeof(dst_dir) - 1); - dst_dir[sizeof(dst_dir) - 1] = '\0'; - if (dl && dst_dir[dl - 1] != '/' && dl < sizeof(dst_dir) - 1) { - dst_dir[dl] = '/'; - dst_dir[dl + 1] = '\0'; - } - } else { - char dnamepat[256]; - split_file_spec(ctx, cmd->params[1], dst_dir, sizeof(dst_dir), - dnamepat, sizeof(dnamepat), - dst_vspec, sizeof(dst_vspec), &dst_hasver); - split_name_type(dnamepat, dname_pat, sizeof(dname_pat), + /* Effective destination VMS spec. A destination that ends with ']' / '>' / + * ':' (a directory or device, no filename) receives each source's own + * name.type; otherwise it is an explicit output filespec (wildcards mapped + * from the source name/type). */ + char dst_eff[1024]; + dcl_rms_effective_spec(ctx, cmd->params[1], dst_eff, sizeof(dst_eff)); + size_t del = strlen(dst_eff); + int dst_is_dir = (del > 0 && (dst_eff[del - 1] == ']' || + dst_eff[del - 1] == '>' || + dst_eff[del - 1] == ':')); + + /* For an explicit output filespec, split it into a directory prefix and the + * output name/type pattern (which may carry "*"/"%" mapped from the source). */ + char dst_prefix[1024] = "", dname_pat[256] = "*", dtype_pat[256] = "*"; + if (!dst_is_dir) { + const char *nt = dst_eff; + const char *rb = strrchr(dst_eff, ']'); + if (!rb) rb = strrchr(dst_eff, '>'); + if (!rb) { const char *cn = strrchr(dst_eff, ':'); if (cn) rb = cn; } + if (rb) { + size_t pl = (size_t)(rb - dst_eff) + 1; + if (pl >= sizeof(dst_prefix)) pl = sizeof(dst_prefix) - 1; + memcpy(dst_prefix, dst_eff, pl); dst_prefix[pl] = '\0'; + nt = rb + 1; + } + char ntbuf[300]; + strncpy(ntbuf, nt, sizeof(ntbuf) - 1); ntbuf[sizeof(ntbuf) - 1] = '\0'; + char *semi = strchr(ntbuf, ';'); if (semi) *semi = '\0'; + split_name_type(ntbuf, dname_pat, sizeof(dname_pat), dtype_pat, sizeof(dtype_pat)); } - struct file_match *m = NULL; - int count = 0; - int st = fm_collect(src_dir, src_pat, &m, &count); - if (st != SS$_NORMAL) { - free(m); + struct dcl_rms_dir *d = dcl_rms_dir_open(ctx, cmd->params[0]); + if (!d) { dcl_error("RMS", 2, "FNF", "file not found - %s", cmd->params[0]); return SS$_NOSUCHFILE; } int copied = 0, matched = 0; - for (int i = 0; i < count; i++) { - if (!version_selected(m, i, count, src_hasver, src_vspec)) continue; + char src_spec[1024]; + while (dcl_rms_dir_next(d, src_spec, sizeof(src_spec), NULL, NULL, NULL)) { matched++; + /* Source name.type (strip device/dir and version). */ + const char *snt = src_spec; + const char *srb = strrchr(src_spec, ']'); + if (!srb) srb = strrchr(src_spec, '>'); + if (srb) snt = srb + 1; + char in_nt[300]; + strncpy(in_nt, snt, sizeof(in_nt) - 1); in_nt[sizeof(in_nt) - 1] = '\0'; + char *isemi = strchr(in_nt, ';'); if (isemi) *isemi = '\0'; + char in_name[256], in_type[256]; - split_name_type(m[i].base, in_name, sizeof(in_name), + split_name_type(in_nt, in_name, sizeof(in_name), in_type, sizeof(in_type)); - char out_name[256], out_type[256]; - map_out_field(dname_pat, in_name, out_name, sizeof(out_name)); - map_out_field(dtype_pat, in_type, out_type, sizeof(out_type)); - - int out_ver = 0; - if (!resolve_out_version(dst_dir, out_name, out_type, - dst_hasver, dst_vspec, new_version, &out_ver)) { - dcl_error("RMS", 2, "FEX", - "file already exists, not superseded - %s.%s;%d", - out_name, out_type, out_ver); - continue; - } - - char dfile[600]; - if (out_ver > 0) { - if (out_type[0]) - snprintf(dfile, sizeof(dfile), "%s.%s;%d", - out_name, out_type, out_ver); - else - snprintf(dfile, sizeof(dfile), "%s;%d", out_name, out_ver); + /* Destination filespec for this source. */ + char dst_spec[1200]; + if (dst_is_dir) { + snprintf(dst_spec, sizeof(dst_spec), "%s%s", dst_eff, in_nt); } else { + char out_name[256], out_type[256]; + map_out_field(dname_pat, in_name, out_name, sizeof(out_name)); + map_out_field(dtype_pat, in_type, out_type, sizeof(out_type)); if (out_type[0]) - snprintf(dfile, sizeof(dfile), "%s.%s", out_name, out_type); + snprintf(dst_spec, sizeof(dst_spec), "%s%s.%s", + dst_prefix, out_name, out_type); else - snprintf(dfile, sizeof(dfile), "%s", out_name); + snprintf(dst_spec, sizeof(dst_spec), "%s%s", dst_prefix, out_name); } - char src_full[2600], dst_full[2600]; - snprintf(src_full, sizeof(src_full), "%s%s", src_dir, m[i].dname); - snprintf(dst_full, sizeof(dst_full), "%s%s", dst_dir, dfile); - if (do_confirm) { - char vsrc[256], vdst[256]; - dcl_format_filespec(src_full, vsrc, sizeof(vsrc)); - dcl_format_filespec(dst_full, vdst, sizeof(vdst)); - printf("COPY %s to %s ? [N]: ", vsrc, vdst); + printf("COPY %s to %s ? [N]: ", src_spec, dst_spec); fflush(stdout); char resp[64]; if (!fgets(resp, sizeof(resp), stdin)) break; if (toupper((unsigned char)resp[0]) != 'Y') continue; } - int cst = copy_one(src_full, dst_full); + int cst = copy_one_rms(ctx, src_spec, dst_spec); if (cst == SS$_NOSUCHFILE) { - dcl_error("RMS", 2, "FNF", "file not found - %s", m[i].dname); + dcl_error("RMS", 2, "FNF", "file not found - %s", src_spec); continue; } if (cst == SS$_FILACCERR) { - dcl_error("RMS", 2, "CRE", "cannot create - %s", dfile); + dcl_error("RMS", 2, "CRE", "cannot create - %s", dst_spec); continue; } if (cst == SS$_ABORT) { - dcl_error("RMS", 2, "WER", "write error - %s", dfile); + dcl_error("RMS", 2, "WER", "write error - %s", dst_spec); continue; } copied++; - if (do_log) { - char vsrc[256], vdst[256]; - dcl_format_filespec(src_full, vsrc, sizeof(vsrc)); - dcl_format_filespec(dst_full, vdst, sizeof(vdst)); - dcl_error("COPY", 1, "COPIED", "%s copied to %s", vsrc, vdst); - } + if (do_log) + dcl_error("COPY", 1, "COPIED", "%s copied to %s", src_spec, dst_spec); } - free(m); + dcl_rms_dir_close(d); if (matched == 0) { dcl_error("RMS", 2, "FNF", "file not found - %s", cmd->params[0]); @@ -1839,60 +2087,30 @@ int cmd_create(struct dcl_command *cmd) /* * VMS CREATE mints a NEW version of the file — it never truncates an - * existing one. So the output version defaults exactly as COPY's does: - * one greater than the highest existing version of the same name.type - * (else the name is written unversioned when none exists yet), and an - * explicit output version ;n that already exists is refused rather than - * silently overwritten. This is the same never-overwrite rule COPY/RENAME - * already follow via resolve_out_version(), applied to CREATE. + * existing one. vms-481: CREATE now writes through RMS ($CREATE + sequential + * $PUT), which on the product runtime reaches the Files-11 ODS-2 ACP over + * /dev/vms. RMS/IO$_CREATE itself allocates a real FID from INDEXF.SYS and + * enters the file in its directory at a new HIGHEST version -- so the + * next-higher-version rule is now the executive's, not an opendir scan. An + * explicit output ;n that already exists is refused by the ACP as RMS$_FEX. + * Fail-honest: no ACP-mounted SYS$DISK => RMS$_CRE/ACC, never a silent + * fopen("w") truncate on a /vms passthrough (INV-6). * * Grounded (clean-room, Rule 8): VSI OpenVMS DCL Dictionary, CREATE — * "creates a sequential disk file" from SYS$INPUT and, like every RMS - * file creation, assigns the next-higher version; and VSI OpenVMS User's - * Manual, "Version Numbers" — creating a file whose name.type already - * exists produces a new, higher version rather than replacing the - * existing one. (Before this fix OVMX CREATE fopen("w")-truncated the - * resolved highest version, silently destroying its contents — vms-73b.) + * file creation, assigns the next-higher version. */ - char cre_dir[1024], cre_pat[512], cre_vspec[64]; - int cre_hasver; - split_file_spec(ctx, cmd->params[0], cre_dir, sizeof(cre_dir), - cre_pat, sizeof(cre_pat), - cre_vspec, sizeof(cre_vspec), &cre_hasver); - - char cre_name[256], cre_type[256]; - split_name_type(cre_pat, cre_name, sizeof(cre_name), - cre_type, sizeof(cre_type)); - - int cre_ver = 0; - if (!resolve_out_version(cre_dir, cre_name, cre_type, - cre_hasver, cre_vspec, /*new_version=*/0, - &cre_ver)) { - dcl_error("RMS", 2, "FEX", - "file already exists, not superseded - %s.%s;%d", - cre_name, cre_type, cre_ver); - return RMS$_FEX; - } - - char cre_file[600]; - if (cre_ver > 0) { - if (cre_type[0]) - snprintf(cre_file, sizeof(cre_file), "%s.%s;%d", - cre_name, cre_type, cre_ver); - else - snprintf(cre_file, sizeof(cre_file), "%s;%d", cre_name, cre_ver); - } else { - if (cre_type[0]) - snprintf(cre_file, sizeof(cre_file), "%s.%s", cre_name, cre_type); - else - snprintf(cre_file, sizeof(cre_file), "%s", cre_name); - } - - char linux_path[2600]; /* dir (<=1024) + name.type;ver (<=600), as COPY */ - snprintf(linux_path, sizeof(linux_path), "%s%s", cre_dir, cre_file); - - FILE *fp = fopen(linux_path, "w"); - if (!fp) { + uint32_t cst = 0; + /* Text file: variable-length records, implied carriage return (VMS default + * for CREATE from SYS$INPUT). MRS 0 = no max. */ + struct dcl_rms_writer *w = + dcl_rms_write_create(ctx, cmd->params[0], FAB$C_VAR, FAB$M_CR, 0, &cst); + if (!w) { + if (cst == RMS$_FEX) { + dcl_error("RMS", 2, "FEX", + "file already exists, not superseded - %s", cmd->params[0]); + return RMS$_FEX; + } dcl_error("RMS", 2, "CRE", "cannot create - %s", cmd->params[0]); return SS$_FILACCERR; } @@ -1906,19 +2124,24 @@ int cmd_create(struct dcl_command *cmd) * idiom, and the consumer of a DECK/EOD block. dcl_sysinput_setup() points * fd 0 at exactly that block (and is a no-op interactively), so the same * read loop serves both. Reference: DCL Dictionary, "CREATE" (input from - * SYS$INPUT); "DECK"/"EOD". (vms-3983) */ + * SYS$INPUT); "DECK"/"EOD". (vms-3983) Each input line becomes one RMS + * record ($PUT), with the trailing newline stripped (RMS records carry no + * terminator; the RAT=CR attribute supplies it on read-back). */ struct dcl_sysinput si; dcl_sysinput_setup(ctx, &si); if (ctx->interactive || ctx->proc_depth >= 0) { char line[4096]; while (1) { if (!fgets(line, sizeof(line), stdin)) break; - fputs(line, fp); + size_t ll = strlen(line); + while (ll > 0 && (line[ll - 1] == '\n' || line[ll - 1] == '\r')) + ll--; + if (dcl_rms_write_record(w, line, ll) != 0) break; } } dcl_sysinput_restore(&si); - fclose(fp); + dcl_rms_write_close(w); return SS$_NORMAL; } diff --git a/src/vmsdcl/dcl_cmd_misc.c b/src/vmsdcl/dcl_cmd_misc.c index f698d3d53..ecc5e2937 100644 --- a/src/vmsdcl/dcl_cmd_misc.c +++ b/src/vmsdcl/dcl_cmd_misc.c @@ -2265,31 +2265,10 @@ int cmd_mount(struct dcl_command *cmd) 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; - } + /* vms-481: the unit is resolved and its ODS-2 home block validated by the + * executive ACP $MOUNT below (vms_kif_acp_mount / vms-127) -- the process no + * longer resolves a backing block device or scans /proc/mounts for a /vms + * passthrough. */ /* Claim the unit in the executive's device table (vms-651 wires * vms_kif_alloc for the first time). */ @@ -2314,75 +2293,34 @@ int cmd_mount(struct dcl_command *cmd) } /* - * 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. + * vms-481: MOUNT the ODS-2 volume EXECUTIVE-GLOBAL through the Files-11 ACP + * ($MOUNT over /dev/vms, vms_kif_acp_mount / vms-127) -- not a setuid-root + * mount(2) of a vmsfs passthrough. The executive reads and validates the + * volume's ODS-2 home block + SCB and records it in the executive-global + * mounted-volume table, so EVERY process that $ASSIGNs the unit sees the + * same mounted volume (design §4.3). This retires the setuid mount(2) + * helper, the /vms mount point, the per-process vmsfs_device_add filespec + * translator, and the DISK$-label-via-helper path -- filespecs against the + * unit now resolve by $ASSIGNing it through the ACP. Fail-honest (INV-6): no + * ACP / no such device => the real SS$_ status, never a passthrough mount. */ - 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) { + uint32_t mst = vms_kif_acp_mount(dev_name); + if (mst == SS$_NOSUCHDEV) { vms_kif_dalloc(dev_name); - dcl_error("SYSTEM", 4, "NOPRIV", - "insufficient privilege or object protection violation"); - return SS$_NOPRIV; + dcl_error("SYSTEM", 0, "NOSUCHDEV", "no such device available"); + return SS$_NOSUCHDEV; } - if (hrc != 0) { + if (mst == SS$_DEVMOUNT) { 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; - - lnm_manager_t *mgr = lnm_get_manager(); - if (mgr) { - lnm_create(mgr, table, log_name, mount_point, - LNM_ATTR_TERMINAL, LNM_MODE_USER); + dcl_error("MOUNT", 2, "DEVMOUNT", + "device already mounted - _%s", dev_name); + return SS$_DEVMOUNT; } - - /* - * DISK$ (vms-f83): VMS MOUNT defines a logical name from the - * volume's own label so the disk can be addressed by label independent of - * the physical unit -- DISK$MYVOL:[DIR]FILE resolves wherever MYVOL is - * mounted (VSI OpenVMS System Manager's Manual, Vol. 1, "Mounting - * Volumes"; VSI OpenVMS DCL Dictionary, MOUNT). The label is READ FROM THE - * VOLUME (its vmsfs home block, via the setuid helper), not the command- - * line token, so the DISK$ name always reflects the disk actually mounted; - * if it cannot be read, no DISK$ logical is defined rather than - * fabricating one (INV-DCL). The equivalence is the device (DKA100:), so - * DISK$