From cc4b1eeac9c48c62b99f1d183677afa9478cb334 Mon Sep 17 00:00:00 2001 From: Joon Lee Date: Thu, 2 Jul 2026 11:24:48 -0400 Subject: [PATCH 1/6] unix: add z/OS implementation of ptrace - Adds a ptrace implementation that provides a linux-compatible api as closely as possible - Core ptrace operations are implemented using z/OS's BPX4PTR syscall. Memory operations use PT_READ_BLOCK/PT_WRITE_BLOCK. Register operations use PT_READ_GPR/PT_WRITE_GPR for 64-bit values. - z/OS lacks native single-step support so it is emulated using temporary breakpoints. - Unsupported operations return ENOSYS --- unix/ptrace_zos.go | 482 ++++++++++++++++++++ unix/ptrace_zos_singlestep.go | 425 ++++++++++++++++++ unix/syscall_zos_test.go | 824 +++++++++++++++++++++++++++++++++- unix/ztypes_zos_s390x.go | 79 ++++ 4 files changed, 1801 insertions(+), 9 deletions(-) create mode 100644 unix/ptrace_zos.go create mode 100644 unix/ptrace_zos_singlestep.go diff --git a/unix/ptrace_zos.go b/unix/ptrace_zos.go new file mode 100644 index 000000000..2e385cf18 --- /dev/null +++ b/unix/ptrace_zos.go @@ -0,0 +1,482 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build zos + +package unix + +import "unsafe" + +// z/OS ptrace implementation notes: +// +// This file provides ptrace wrappers for z/OS that match the Linux API as closely +// as possible. However, there are fundamental differences in how z/OS implements +// process tracing compared to Linux: +// +// 1. SYSTEM CALL INTERFACE: +// - z/OS uses BPX4PTR (5 parameters) vs Linux ptrace syscall (4 parameters) +// - The 5th parameter (buffer) is used for block operations +// +// 2. MEMORY OPERATIONS: +// - z/OS: PT_READ_BLOCK/PT_WRITE_BLOCK for efficient block transfers +// - Linux: PTRACE_PEEKTEXT/POKETEXT for word-by-word access +// +// 3. REGISTER OPERATIONS: +// - z/OS: PT_READ_GPR/PT_WRITE_GPR with register number for 64-bit values +// - Linux: PTRACE_GETREGSET/SETREGSET with iovec structure +// +// 4. SINGLE-STEPPING: +// - z/OS: No native single-step; requires temporary breakpoints or PER +// - Linux: PTRACE_SINGLESTEP natively supported +// a) Read instruction at current PSW +// b) Calculate next instruction address (handle branches) +// c) Set temporary breakpoint (SVC 144 = 0x0A90) at next address +// d) Continue execution until breakpoint hit +// e) Restore original instruction and adjust PSW +// +// 5. SYSCALL TRACING: +// - z/OS: No PTRACE_SYSCALL equivalent; requires breakpoint-based approach +// - Linux: PTRACE_SYSCALL stops at syscall entry/exit +// a) Identify BPX syscall vector location +// b) Set breakpoints at syscall entry points +// c) Detect BASR instructions calling into syscall vector +// d) Parse arguments from registers before/after syscall +// +// 6. LIMITATIONS: +// - PtraceSetOptions: No equivalent (returns ENOSYS) +// - PtraceGetEventMsg: No equivalent (returns ENOSYS) +// - PtraceSingleStep: Requires complex implementation (returns ENOSYS) +// - PtraceInterrupt: No equivalent (returns ENOSYS) +// - PtracePokeUser: No PT_WRITE_U (returns ENOSYS) +// +// For reference implementations of single-stepping and syscall tracing on z/OS, + +// ptrace is the basic wrapper for BPX4PTR on z/OS. +// Note: z/OS ptrace (BPX4PTR) takes 5 parameters vs 4 on Linux/Darwin. +// The 5th parameter (buffer) is used for operations like PT_READ_BLOCK/PT_WRITE_BLOCK. +func ptrace(request int, pid int, addr uintptr, data uintptr) (err error) { + return ptracePtr(request, pid, addr, unsafe.Pointer(data)) +} + +// ptracePtr is a variant that accepts unsafe.Pointer for the data parameter. +func ptracePtr(request int, pid int, addr uintptr, data unsafe.Pointer) (err error) { + rv, rc, rn := Bpx4ptr(int32(request), int32(pid), + unsafe.Pointer(addr), data, nil) + if rv != 0 { + err = errnoErr2(Errno(rc), uintptr(rn)) + } + return +} + +// ptracePtrWithBuffer is used for operations that require the buffer parameter. +func ptracePtrWithBuffer(request int, pid int, addr uintptr, data unsafe.Pointer, buffer unsafe.Pointer) (err error) { + rv, rc, rn := Bpx4ptr(int32(request), int32(pid), + unsafe.Pointer(addr), data, buffer) + if rv != 0 { + err = errnoErr2(Errno(rc), uintptr(rn)) + } + return +} + +// High-level convenience functions matching the Linux/Darwin API + +func PtraceAttach(pid int) (err error) { + return ptrace(PT_ATTACH, pid, 0, 0) +} + +func PtraceDetach(pid int) (err error) { + return ptrace(PT_DETACH, pid, 0, 0) +} + +func PtraceCont(pid int, signal int) (err error) { + // PT_CONTINUE can be interrupted by signals (EINTR), retry if needed + for { + err = ptrace(PT_CONTINUE, pid, 0, uintptr(signal)) + if err != EINTR { + break + } + } + return err +} + +func PtraceKill(pid int) (err error) { + return ptrace(PT_KILL, pid, 0, 0) +} + +// ptracePeek implements the peek operations using PT_READ_BLOCK. +// On z/OS, PT_READ_BLOCK can read arbitrary-length data efficiently. +// Unlike Linux which reads word-by-word, z/OS can read blocks directly. +func ptracePeek(req int, pid int, addr uintptr, out []byte) (count int, err error) { + if len(out) == 0 { + return 0, nil + } + + // z/OS PT_READ_BLOCK can read blocks directly without word-by-word alignment + // The buffer parameter points to the output buffer + buffer := unsafe.Pointer(&out[0]) + err = ptracePtrWithBuffer(req, pid, addr, unsafe.Pointer(uintptr(len(out))), buffer) + if err != nil { + return 0, err + } + return len(out), nil +} + +// PtracePeekText reads from the traced process's text segment. +func PtracePeekText(pid int, addr uintptr, out []byte) (count int, err error) { + return ptracePeek(PT_READ_BLOCK, pid, addr, out) +} + +// PtracePeekData reads from the traced process's data segment. +func PtracePeekData(pid int, addr uintptr, out []byte) (count int, err error) { + return ptracePeek(PT_READ_BLOCK, pid, addr, out) +} + +// PtracePeekUser reads from the traced process's user area. +// On z/OS, PT_READ_U reads a 32-bit word from the user area at the specified offset. +// To match Linux behavior, we implement word-by-word reading for arbitrary lengths. +func PtracePeekUser(pid int, addr uintptr, out []byte) (count int, err error) { + if len(out) == 0 { + return 0, nil + } + + // PT_READ_U reads 32-bit words from user area + // We need to read word-by-word to handle arbitrary lengths + n := 0 + for n < len(out) { + var data uint32 + err = ptracePtr(PT_READ_U, pid, addr+uintptr(n), unsafe.Pointer(&data)) + if err != nil { + return n, err + } + + // Copy up to 4 bytes to output + remaining := len(out) - n + if remaining >= 4 { + out[n] = byte(data >> 24) + out[n+1] = byte(data >> 16) + out[n+2] = byte(data >> 8) + out[n+3] = byte(data) + n += 4 + } else { + // Handle partial word at the end + for i := 0; i < remaining; i++ { + out[n+i] = byte(data >> (24 - i*8)) + } + n += remaining + } + } + return n, nil +} + +// ptracePoke implements the poke operations using PT_WRITE_BLOCK. +func ptracePoke(pokeReq int, peekReq int, pid int, addr uintptr, data []byte) (count int, err error) { + if len(data) == 0 { + return 0, nil + } + + // z/OS PT_WRITE_BLOCK can write blocks directly + buffer := unsafe.Pointer(&data[0]) + err = ptracePtrWithBuffer(pokeReq, pid, addr, unsafe.Pointer(uintptr(len(data))), buffer) + if err != nil { + return 0, err + } + return len(data), nil +} + +// PtracePokeText writes to the traced process's text segment. +func PtracePokeText(pid int, addr uintptr, data []byte) (count int, err error) { + return ptracePoke(PT_WRITE_BLOCK, PT_READ_BLOCK, pid, addr, data) +} + +// PtracePokeData writes to the traced process's data segment. +func PtracePokeData(pid int, addr uintptr, data []byte) (count int, err error) { + return ptracePoke(PT_WRITE_BLOCK, PT_READ_BLOCK, pid, addr, data) +} + +// PtracePokeUser writes to the traced process's user area. +// Note: z/OS does not have a direct PT_WRITE_U equivalent. +// This function returns ENOSYS to indicate it's not supported. +// DIFFERENCE FROM LINUX: Linux supports PTRACE_POKEUSR, z/OS does not. +func PtracePokeUser(pid int, addr uintptr, data []byte) (count int, err error) { + // z/OS doesn't have PT_WRITE_U + return 0, ENOSYS +} + +// PtraceGetRegs retrieves the general purpose registers from the traced process. +// On z/OS, PT_READ_GPR can read individual registers as 64-bit values when called +// with a register number, or read all registers when called with a buffer. +// This function reads each register individually to ensure full 64-bit values. +// DIFFERENCE FROM LINUX: Linux uses PTRACE_GETREGSET with iovec, z/OS uses PT_READ_GPR. +func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) { + // Use PT_REGHSET to enable reading high GPRs for AMODE 64 programs + rv, rc, rn := Bpx4ptr(int32(PT_REGHSET), int32(pid), nil, nil, nil) + _ = rv + _ = rc + _ = rn + + // Use PT_BLOCKREQ to read GPRs and PSW in one call + // dbx uses 3 requests: PT_READ_GPR, PT_READ_U, PT_READ_GPRH + // PT_READ_U is required for ptrace to populate PSW in GPR block + // + // IMPORTANT: z/OS ptrace always returns 64-bit ESA/390 format PSW in the Psw field. + // The 128-bit Pswg field is never populated, even with PSWG_Req bit set. + // We convert ESA/390 to z/Architecture format in software (matching DBX behavior). + const ( + numRequests = 3 + reqSize = int(unsafe.Sizeof(PtraceBlkReqReq{})) + gprSize = int(unsafe.Sizeof(PtraceBlkGpr{})) + uareaSize = int(unsafe.Sizeof(PtraceBlkUar{})) + 6*int(unsafe.Sizeof(PtraceBlkUarOcw{})) + totalSize = int(unsafe.Sizeof(PtraceBlkReq{})) + numRequests*reqSize + gprSize*2 + uareaSize + ) + + // Allocate buffer for block request + buf := make([]byte, totalSize) + + // Setup block request header + blkReq := (*PtraceBlkReq)(unsafe.Pointer(&buf[0])) + blkReq.Numreq = numRequests + + // Setup request array + reqOffset := int(unsafe.Sizeof(PtraceBlkReq{})) + reqs := (*[3]PtraceBlkReqReq)(unsafe.Pointer(&buf[reqOffset])) + + // Setup GPR block (lower 32 bits) + gprOffset := reqOffset + numRequests*reqSize + gprBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprOffset])) + + // Setup user area block (required for PSW) + uareaOffset := gprOffset + gprSize + uareaBlock := (*PtraceBlkUar)(unsafe.Pointer(&buf[uareaOffset])) + uareaBlock.Num = 6 // Number of control info entries + + // Setup user area offset/control words + uareaOcw := (*[6]PtraceBlkUarOcw)(unsafe.Pointer(&buf[uareaOffset + int(unsafe.Sizeof(PtraceBlkUar{}))])) + uareaOcw[0].Ofs = 1025 // program interrupt code + uareaOcw[1].Ofs = 1026 // abend completion code + uareaOcw[2].Ofs = 1027 // abend reason code + uareaOcw[3].Ofs = 1028 // signal code + uareaOcw[4].Ofs = 1029 // instruction length code + uareaOcw[5].Ofs = 1030 // process flags + + // Setup high GPR block (upper 32 bits) + gprHighOffset := uareaOffset + uareaSize + gprHighBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprHighOffset])) + + // Configure requests (order matters: GPR, U, GPRH) + // reqdata contains OFFSET from buffer start, not absolute address + // dbx: brr[0].reqdata=(unsigned long)brg; where brg is the offset + reqs[0].Reqtype = PT_READ_GPR + reqs[0].Reqdata = uint32(gprOffset) + reqs[1].Reqtype = PT_READ_U + reqs[1].Reqdata = uint32(uareaOffset) + reqs[2].Reqtype = PT_READ_GPRH + reqs[2].Reqdata = uint32(gprHighOffset) + + // Execute block request + rvBlk, rcBlk, rnBlk := Bpx4ptr(int32(PT_BLOCKREQ), int32(pid), + unsafe.Pointer(&buf[0]), unsafe.Pointer(uintptr(totalSize)), nil) + if rvBlk == -1 { + return errnoErr2(Errno(rcBlk), uintptr(rnBlk)) + } + + // Extract GPRs (combine low and high 32 bits) + for i := 0; i < 16; i++ { + low := uint64(gprBlock.Gpr[i]) + high := uint64(gprHighBlock.Gpr[i]) << 32 + regsout.Gprs[i] = high | low + } + + // Extract PSW from GPR block structure + // z/OS ptrace ALWAYS returns 8-byte ESA/390 format PSW at offset 144. + // The 16-byte PSWG field at offset 152 is NEVER populated (always zero). + // We must convert ESA/390 format to z/Architecture format in software (like DBX does). + // + // ESA/390 PSW format (8 bytes): + // Word 1 (bytes 0-3): Mask with bit 12 = 1 (ECMODE31BIT) + // Word 2 (bytes 4-7): Address with bit 0 = AMODE bit (1=31-bit, 0=64-bit) + // + // z/Architecture PSW format (16 bytes): + // Mask (bytes 0-7): Extended mask with bit 12 = 0, bits 31-32 = EA+BA (AMODE) + // Addr (bytes 8-15): Full 64-bit instruction address + + // Read ESA/390 PSW as two 32-bit words + pswWord1 := uint32(gprBlock.Psw[0])<<24 | uint32(gprBlock.Psw[1])<<16 | + uint32(gprBlock.Psw[2])<<8 | uint32(gprBlock.Psw[3]) + pswWord2 := uint32(gprBlock.Psw[4])<<24 | uint32(gprBlock.Psw[5])<<16 | + uint32(gprBlock.Psw[6])<<8 | uint32(gprBlock.Psw[7]) + + // Convert ESA/390 to z/Architecture format (matching DBX psw_ESA390_to_zArchitecture) + const ECMODE31BIT = uint64(0x0000000000080000) + const AMODE31BIT = uint64(0x0000000080000000) + const ADDR_MASK_31 = uint64(0x000000007FFFFFFF) + + // Extract AMODE bit from address word (bit 0 of word 2) + amode31 := uint64(pswWord2) & AMODE31BIT + + // Mask off AMODE bit from address + addr := uint64(pswWord2) & ADDR_MASK_31 + + // Convert mask: clear ECMODE31BIT, shift left 32 bits, add AMODE bit + mask := uint64(pswWord1) & ^ECMODE31BIT + mask = (mask << 32) | amode31 + + regsout.Psw.Mask = mask + regsout.Psw.Addr = addr + + // Note: Access registers (Acrs), floating point registers (Fp_regs), + // PER info (Per_info), and other fields would require additional + // PT_READ_* calls. For now, we focus on GPRs and PSW which are + // the most commonly used registers for debugging. + + return nil +} + +// ptraceWritePSW writes only the PSW address register. +// This is a lower-level function used internally for single-stepping. +// Based on ztrace's write_psw function. +func ptraceWritePSW(pid int, pswAddr uint32) error { + const PTRACE_REG_PSWA = 41 + rv, rc, rn := Bpx4ptr(int32(PT_WRITE_GPR), int32(pid), + unsafe.Pointer(uintptr(PTRACE_REG_PSWA)), + unsafe.Pointer(uintptr(pswAddr)), + nil) + if rv == -1 { + return errnoErr2(Errno(rc), uintptr(rn)) + } + return nil +} + +// PtraceSetRegs sets the general purpose registers in the traced process. +// Uses PT_BLOCKREQ to write GPRs and PSW in a single call, matching dbx implementation. +// DIFFERENCE FROM LINUX: Linux uses PTRACE_SETREGSET with iovec, z/OS uses PT_BLOCKREQ. +// NOTE: The process must be in a stopped state for register writes to succeed. +func PtraceSetRegs(pid int, regs *PtraceRegs) (err error) { + // Use PT_REGHSET to enable writing high GPRs for AMODE 64 programs + rv, rc, rn := Bpx4ptr(int32(PT_REGHSET), int32(pid), nil, nil, nil) + _ = rv + _ = rc + _ = rn + + // Use PT_BLOCKREQ to write GPRs and PSW in one call + const ( + numRequests = 2 + reqSize = int(unsafe.Sizeof(PtraceBlkReqReq{})) + gprSize = int(unsafe.Sizeof(PtraceBlkGpr{})) + totalSize = int(unsafe.Sizeof(PtraceBlkReq{})) + numRequests*reqSize + gprSize*2 + ) + + // Allocate buffer for block request + buf := make([]byte, totalSize) + + // Setup block request header + blkReq := (*PtraceBlkReq)(unsafe.Pointer(&buf[0])) + blkReq.Numreq = numRequests + + // Setup request array + reqOffset := int(unsafe.Sizeof(PtraceBlkReq{})) + reqs := (*[2]PtraceBlkReqReq)(unsafe.Pointer(&buf[reqOffset])) + + // Setup GPR block (lower 32 bits) + gprOffset := reqOffset + numRequests*reqSize + gprBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprOffset])) + + // Setup high GPR block (upper 32 bits) + gprHighOffset := gprOffset + gprSize + gprHighBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprHighOffset])) + + // Mark all GPRs as modified (set all 16 bits) + gprBlock.Writebitflags = 0xFFFF + gprHighBlock.Writebitflags = 0xFFFF + + // Split 64-bit GPRs into low and high 32 bits + for i := 0; i < 16; i++ { + gprBlock.Gpr[i] = uint32(regs.Gprs[i] & 0xFFFFFFFF) + gprHighBlock.Gpr[i] = uint32(regs.Gprs[i] >> 32) + } + + // Pack PSW for 31-bit targets (current z/OS) + // Upper 32 bits = mask, lower 32 bits = address + gprBlock.Wpsw = 1 // Mark PSW as modified + // Write PSW in old format (8 bytes at offset 144) + // Word 1 (mask) at bytes 0-3, Word 2 (addr) at bytes 4-7 + pswMask := uint32(regs.Psw.Mask) + pswAddr := uint32(regs.Psw.Addr) + gprBlock.Psw[0] = byte(pswMask >> 24) + gprBlock.Psw[1] = byte(pswMask >> 16) + gprBlock.Psw[2] = byte(pswMask >> 8) + gprBlock.Psw[3] = byte(pswMask) + gprBlock.Psw[4] = byte(pswAddr >> 24) + gprBlock.Psw[5] = byte(pswAddr >> 16) + gprBlock.Psw[6] = byte(pswAddr >> 8) + gprBlock.Psw[7] = byte(pswAddr) + + // Clear extended PSWG (not used for 31-bit targets) + for i := 0; i < 16; i++ { + gprBlock.Pswg[i] = 0 + } + + // Configure requests + // reqdata contains OFFSET from buffer start, not absolute address + reqs[0].Reqtype = PT_WRITE_GPR + reqs[0].Reqdata = uint32(gprOffset) + reqs[1].Reqtype = PT_WRITE_GPRH + reqs[1].Reqdata = uint32(gprHighOffset) + + // Execute block request + rvBlk, rcBlk, rnBlk := Bpx4ptr(int32(PT_BLOCKREQ), int32(pid), + unsafe.Pointer(&buf[0]), unsafe.Pointer(uintptr(totalSize)), nil) + if rvBlk == -1 { + return errnoErr2(Errno(rcBlk), uintptr(rnBlk)) + } + + return nil +} + +// PtraceSetOptions sets ptrace options. +// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_SETOPTIONS equivalent. +// Returns ENOSYS to indicate this is not supported on z/OS. +func PtraceSetOptions(pid int, options int) (err error) { + return ENOSYS +} + +// PtraceGetEventMsg retrieves a message about the ptrace event. +// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_GETEVENTMSG equivalent. +// Returns ENOSYS to indicate this is not supported on z/OS. +func PtraceGetEventMsg(pid int) (msg uint, err error) { + return 0, ENOSYS +} + +// PtraceSyscall continues execution and stops at the next syscall entry/exit. +// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_SYSCALL equivalent. +// This function falls back to PT_CONTINUE and will NOT stop at syscalls. +// +// To trace syscalls on z/OS, applications must: +// 1. Set breakpoints at BPX syscall entry points (requires knowledge of syscall vector) +// 2. Detect BASR instructions that call into the BPX syscall vector +// 3. Use PT_LDINFO to identify loaded modules and their entry points +// +func PtraceSyscall(pid int, signal int) (err error) { + // z/OS doesn't have a direct PTRACE_SYSCALL equivalent + // Use PT_CONTINUE as a fallback (won't stop at syscalls) + return ptrace(PT_CONTINUE, pid, 0, uintptr(signal)) +} + +// Note: PtraceSingleStep is implemented in ptrace_zos_singlestep.go +// with full two-breakpoint emulation for branch instructions. + +// PtraceInterrupt interrupts the traced process. +// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_INTERRUPT equivalent. +// Returns ENOSYS to indicate this is not supported on z/OS. +func PtraceInterrupt(pid int) (err error) { + return ENOSYS +} + +// PtraceSeize attaches to a process without stopping it. +// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_SEIZE equivalent. +// Uses PT_ATTACH as a fallback, which will stop the process. +func PtraceSeize(pid int) (err error) { + // z/OS doesn't have PTRACE_SEIZE, use PT_ATTACH + // Note: This WILL stop the process, unlike Linux PTRACE_SEIZE + return ptrace(PT_ATTACH, pid, 0, 0) +} diff --git a/unix/ptrace_zos_singlestep.go b/unix/ptrace_zos_singlestep.go new file mode 100644 index 000000000..7b44d3b95 --- /dev/null +++ b/unix/ptrace_zos_singlestep.go @@ -0,0 +1,425 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build zos + +package unix + +import ( + "sync" +) + +// Single-stepping emulation for z/OS +// +// z/OS does not have native single-step support like Linux's PTRACE_SINGLESTEP. +// Instead, single-stepping must be emulated using temporary breakpoints. +// +// This file provides a complete implementation of single-step emulation using +// temporary breakpoints at both the sequential next instruction and branch target. + +const ( + // NO_BRANCH_DEST indicates the instruction is not a branch or cannot branch + NO_BRANCH_DEST = 0xFFFFFFFF + + // SVC_144 is the breakpoint instruction (SVC 144 = 0x0A90) + SVC_144 = 0x0A90 + + // Maximum number of temporary breakpoints (one for next PC, one for branch dest) + MAX_TEMP_BREAKPOINTS = 2 + + // S/390 branch instruction opcodes + // RR format (Register-Register) + opBCR = 0x07 // Branch on Condition Register + opBALR = 0x05 // Branch and Link Register + opBASR = 0x0D // Branch and Save Register + opBASSM = 0x0C // Branch and Save and Set Mode + opBSM = 0x0B // Branch and Set Mode + opBCTR = 0x06 // Branch on Count Register + + // RX format (Register-Index-Storage) + opBC = 0x47 // Branch on Condition + opBAL = 0x45 // Branch and Link + opBCT = 0x46 // Branch on Count + opBAS = 0x4D // Branch and Save + + // RS format (Register-Storage) + opBXH = 0x86 // Branch on Index High + opBXLE = 0x87 // Branch on Index Low or Equal + + // Extended branch opcodes + opBA7 = 0xA7 // A7x series (BRC, BRAS, BRCT, BRCTG) + opBC0 = 0xC0 // C0x series (BRASL, BRCL) + opBEC = 0xEC // ECxx series (RIEc format: compare-and-branch) +) + +// tempBreakpoint represents a temporary breakpoint for single-stepping +type tempBreakpoint struct { + address uint64 + originalInsn [8]byte // Save up to 8 bytes (full instruction) + active bool +} + +// singleStepState manages temporary breakpoints for a process +type singleStepState struct { + mu sync.Mutex + breakpoints [MAX_TEMP_BREAKPOINTS]tempBreakpoint +} + +var ( + // Global map of process states (pid -> state) + processStates = make(map[int]*singleStepState) + processStatesMu sync.Mutex +) + +// getProcessState returns or creates the single-step state for a process +func getProcessState(pid int) *singleStepState { + processStatesMu.Lock() + defer processStatesMu.Unlock() + + state, exists := processStates[pid] + if !exists { + state = &singleStepState{} + processStates[pid] = state + } + return state +} + +// cleanupProcessState removes the state for a process (call on detach) +func cleanupProcessState(pid int) { + processStatesMu.Lock() + defer processStatesMu.Unlock() + delete(processStates, pid) +} + +// GetInstructionLength returns the length of an S/390 instruction in bytes. +func GetInstructionLength(firstByte byte) int { + switch (firstByte >> 6) & 0x03 { + case 0x00: + return 2 + case 0x01, 0x02: + return 4 + case 0x03: + return 6 + default: + return 2 + } +} + +// GetNextInstructionAddr returns the address of the next sequential instruction. +func GetNextInstructionAddr(pc uint64, instruction []byte) uint64 { + if len(instruction) < 1 { + return pc + 2 + } + insnLen := GetInstructionLength(instruction[0]) + return pc + uint64(insnLen) +} + +// CalculateBranchDest calculates the destination address for branch instructions. +func CalculateBranchDest(pid int, pc uint64, instruction []byte) uint64 { + if len(instruction) < 6 { + return NO_BRANCH_DEST + } + + inst0_15 := uint16(instruction[0])<<8 | uint16(instruction[1]) + inst16_31 := uint16(instruction[2])<<8 | uint16(instruction[3]) + inst32_47 := uint16(instruction[4])<<8 | uint16(instruction[5]) + opcode := instruction[0] + + var dest uint64 = NO_BRANCH_DEST + + // RR format branches + if opcode == opBCR || opcode == opBALR || opcode == opBASR || + opcode == opBASSM || opcode == opBSM || opcode == opBCTR { + + r2 := int(inst0_15 & 0x0F) + + if opcode == opBCR { + mask := int((inst0_15 >> 4) & 0x0F) + if mask == 0 || r2 == 0 { + return NO_BRANCH_DEST + } + } else { + if r2 == 0 { + return NO_BRANCH_DEST + } + } + + var regs PtraceRegs + if err := PtraceGetRegs(pid, ®s); err == nil { + if r2 >= 0 && r2 < 16 { + regVal := regs.Gprs[r2] + dest = regVal + } + } + } else if opcode == opBC || opcode == opBAL || opcode == opBCT || opcode == opBAS { + // RX format branches + if opcode == opBC { + mask := int((inst0_15 >> 4) & 0x0F) + if mask == 0 { + return NO_BRANCH_DEST + } + } + + index := int(inst0_15 & 0x0F) + base := int((inst16_31 >> 12) & 0x0F) + displacement := uint32(inst16_31 & 0x0FFF) + + var baseVal, indexVal uint64 + var regs PtraceRegs + if err := PtraceGetRegs(pid, ®s); err == nil { + if base != 0 && base < 16 { + baseVal = regs.Gprs[base] + } + if index != 0 && index < 16 { + indexVal = regs.Gprs[index] + } + } + + if base == 0 && index == 0 { + dest = pc + uint64(displacement) + } else { + dest = baseVal + indexVal + uint64(displacement) + } + } else if opcode == opBXH || opcode == opBXLE { + // RS format branches + base := int((inst16_31 >> 12) & 0x0F) + displacement := uint32(inst16_31 & 0x0FFF) + + var baseVal uint64 + var regs PtraceRegs + if err := PtraceGetRegs(pid, ®s); err == nil { + if base != 0 && base < 16 { + baseVal = regs.Gprs[base] + } + } + + dest = baseVal + uint64(displacement) + } else if opcode == opBA7 { + // RI format branches + subOp := int(inst0_15 & 0x0F) + + if subOp < 4 || subOp > 7 { + return NO_BRANCH_DEST + } + + if subOp == 0x04 { + mask := int((inst0_15 >> 4) & 0x0F) + if mask == 0 { + return NO_BRANCH_DEST + } + } + + relDispl := int16(inst16_31) + dest = uint64(int64(pc) + int64(relDispl)*2) + } else if opcode == opBC0 { + // RIL format branches + subOp := int(inst0_15 & 0x0F) + + if subOp == 0x04 || subOp == 0x05 { + if subOp == 0x04 { + mask := int((inst0_15 >> 4) & 0x0F) + if mask == 0 { + return NO_BRANCH_DEST + } + } + + inst16_47 := (uint32(inst16_31) << 16) | uint32(inst32_47) + relDispl4 := int32(inst16_47) + dest = uint64(int64(pc) + int64(relDispl4)*2) + } + } else if opcode == opBEC { + // RIEc format compare-and-branch + subOp := instruction[4] + + isCompareBranch := (subOp >= 0x64 && subOp <= 0x67) || + (subOp >= 0x76 && subOp <= 0x77) || + (subOp >= 0x7C && subOp <= 0x7F) + + if isCompareBranch { + ri4 := int16(inst16_31) + dest = uint64(int64(pc) + int64(ri4)*2) + } + } + + if dest < 0x2000 { + return NO_BRANCH_DEST + } + + return dest +} + +// setTempBreakpoint sets a temporary breakpoint at the specified address +func (s *singleStepState) setTempBreakpoint(pid int, addr uint64) error { + s.mu.Lock() + defer s.mu.Unlock() + + // Find free slot + slot := -1 + for i := 0; i < MAX_TEMP_BREAKPOINTS; i++ { + if !s.breakpoints[i].active { + slot = i + break + } + } + + if slot == -1 { + return ENOMEM // All slots in use + } + + // Read original instruction (up to 8 bytes for safety) + _, err := PtracePeekText(pid, uintptr(addr), s.breakpoints[slot].originalInsn[:]) + if err != nil { + return err + } + + // Write SVC 144 (0x0A90) as breakpoint + svc144 := []byte{0x0A, 0x90} + _, err = PtracePokeText(pid, uintptr(addr), svc144) + if err != nil { + return err + } + + s.breakpoints[slot].address = addr + s.breakpoints[slot].active = true + + return nil +} + +// removeTempBreakpoints removes all active temporary breakpoints +func (s *singleStepState) removeTempBreakpoints(pid int) error { + s.mu.Lock() + defer s.mu.Unlock() + + var lastErr error + for i := 0; i < MAX_TEMP_BREAKPOINTS; i++ { + if !s.breakpoints[i].active { + continue + } + + // Restore original instruction (first 2 bytes are enough for SVC 144) + _, err := PtracePokeText(pid, uintptr(s.breakpoints[i].address), s.breakpoints[i].originalInsn[:2]) + if err != nil { + lastErr = err + } + + s.breakpoints[i].active = false + } + + return lastErr +} + +// isTempBreakpoint checks if an address is a temporary breakpoint +func (s *singleStepState) isTempBreakpoint(addr uint64) bool { + s.mu.Lock() + defer s.mu.Unlock() + + for i := 0; i < MAX_TEMP_BREAKPOINTS; i++ { + if s.breakpoints[i].active && s.breakpoints[i].address == addr { + return true + } + } + return false +} + +// PtraceSingleStep executes a single instruction using temporary breakpoints. +// +// This function: +// 1. Reads the current instruction at PSW +// 2. Calculates the next instruction address (sequential) +// 3. If branch instruction, calculates branch destination +// 4. Sets temporary breakpoints at both addresses +// 5. Continues execution until breakpoint hit +// 6. Restores original instructions and adjusts PSW +// +// Returns nil on success, error on failure. +func PtraceSingleStep(pid int) error { + state := getProcessState(pid) + + // 1. Get current PSW + var regs PtraceRegs + if err := PtraceGetRegs(pid, ®s); err != nil { + return err + } + pc := regs.Psw.Addr + + // 2. Read instruction at PC + insn := make([]byte, 8) + if _, err := PtracePeekText(pid, uintptr(pc), insn); err != nil { + return err + } + + // 3. Calculate next instruction addresses + nextPC := GetNextInstructionAddr(pc, insn) + branchDest := CalculateBranchDest(pid, pc, insn) + + // 4. Set temporary breakpoints + if err := state.setTempBreakpoint(pid, nextPC); err != nil { + return err + } + + // If branch instruction, set breakpoint at branch destination too + if branchDest != NO_BRANCH_DEST && branchDest != nextPC { + if err := state.setTempBreakpoint(pid, branchDest); err != nil { + state.removeTempBreakpoints(pid) // Cleanup first breakpoint + return err + } + } + + // 5. Continue execution + if err := PtraceCont(pid, 0); err != nil { + state.removeTempBreakpoints(pid) + return err + } + + // 6. Wait for breakpoint hit (retry on EINTR) + var status WaitStatus + for { + _, err := Wait4(pid, &status, 0, nil) + if err == EINTR { + continue // Retry on interrupted system call + } + if err != nil { + state.removeTempBreakpoints(pid) + return err + } + break + } + + // Check if process stopped (should be at breakpoint) + if !status.Stopped() { + state.removeTempBreakpoints(pid) + return ECHILD // Process exited or error + } + + // 7. Get current PSW (should be at breakpoint + 2) + if err := PtraceGetRegs(pid, ®s); err != nil { + state.removeTempBreakpoints(pid) + return err + } + + // 8. Remove temporary breakpoints + if err := state.removeTempBreakpoints(pid); err != nil { + return err + } + + // 9. Back up PSW by 2 bytes (to point at original instruction, not SVC 144) + // Use direct PSW write like ztrace does, not full register set + hitAddr := regs.Psw.Addr + if state.isTempBreakpoint(hitAddr - 2) { + if err := ptraceWritePSW(pid, uint32(hitAddr - 2)); err != nil { + return err + } + } + + return nil +} + +// PtraceDetachWithCleanup detaches from a process and cleans up single-step state. +// Use this instead of PtraceDetach when using PtraceSingleStep. +func PtraceDetachWithCleanup(pid int) error { + state := getProcessState(pid) + state.removeTempBreakpoints(pid) + cleanupProcessState(pid) + return PtraceDetach(pid) +} \ No newline at end of file diff --git a/unix/syscall_zos_test.go b/unix/syscall_zos_test.go index fad374465..341c1810c 100644 --- a/unix/syscall_zos_test.go +++ b/unix/syscall_zos_test.go @@ -3259,25 +3259,831 @@ func TestFstatat(t *testing.T) { func TestFreezeUnfreeze(t *testing.T) { rv, rc, rn := unix.Bpx4ptq(unix.QUIESCE_FREEZE, "FREEZE") if rc != 0 { - t.Fatalf(fmt.Sprintf("Bpx4ptq FREEZE %v %v %v\n", rv, rc, rn)) + t.Fatalf("%s", fmt.Sprintf("Bpx4ptq FREEZE %v %v %v\n", rv, rc, rn)) } rv, rc, rn = unix.Bpx4ptq(unix.QUIESCE_UNFREEZE, "UNFREEZE") if rc != 0 { - t.Fatalf(fmt.Sprintf("Bpx4ptq UNFREEZE %v %v %v\n", rv, rc, rn)) + t.Fatalf("%s", fmt.Sprintf("Bpx4ptq UNFREEZE %v %v %v\n", rv, rc, rn)) } } func TestPtrace(t *testing.T) { - cmd := exec.Command("/bin/sleep", "1000") + // Create a test program that stays in user code (AMODE 64) without system calls + // usleep() switches to AMODE 31 in system code, so we use a pure busy loop + testProg := ` +int main() { + volatile long long counter = 0; + volatile long long dummy = 0; + // Pure user-space loop - stays in AMODE 64 + while (1) { + counter++; + // Add some work to slow down the loop without system calls + for (int i = 0; i < 10000; i++) { + dummy += i; + } + if (counter > 1000000) counter = 0; + } + return 0; +} +` + // Write test program to temp file + tmpDir := t.TempDir() + srcFile := tmpDir + "/ptrace_test.c" + binFile := tmpDir + "/ptrace_test" + + err := os.WriteFile(srcFile, []byte(testProg), 0644) + if err != nil { + t.Fatalf("Failed to write test program: %v", err) + } + + // Compile test program as 64-bit to test full 64-bit address support + compileCmd := exec.Command("xlc", "-q64", "-o", binFile, srcFile) + compileOut, err := compileCmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed to compile test program: %v\nOutput: %s", err, compileOut) + } + + // Verify the executable is actually AMODE 64 + fileCmd := exec.Command("/bin/file", binFile) + fileOut, err := fileCmd.CombinedOutput() + if err != nil { + t.Logf("Warning: Could not verify executable type: %v", err) + } else { + t.Logf("Executable type: %s", string(fileOut)) + } + + // Start the test program with STEPLIB set for 64-bit runtime libraries + cmd := exec.Command(binFile) cmd.Stdout = os.Stdout - err := cmd.Start() + // CEE.SCEERUN2 is the 64-bit runtime library on z/OS + cmd.Env = append(os.Environ(), "STEPLIB=CEE.SCEERUN2") + err = cmd.Start() if err != nil { - log.Fatal(err) + t.Fatalf("Failed to start child process: %v", err) } - rv, rc, rn := unix.Bpx4ptr(unix.PT_ATTACH, int32(cmd.Process.Pid), unsafe.Pointer(uintptr(0)), unsafe.Pointer(uintptr(0)), unsafe.Pointer(uintptr(0))) - if rc != 0 { - t.Fatalf("ptrace: Bpx4ptr rv %d, rc %d, rn %d\n", rv, rc, rn) + defer cmd.Process.Kill() + + pid := cmd.Process.Pid + attached := false + + // Give the process a moment to start executing user code + time.Sleep(50 * time.Millisecond) + + // Test PtraceAttach + t.Run("Attach", func(t *testing.T) { + err := unix.PtraceAttach(pid) + if err != nil { + t.Fatalf("PtraceAttach failed: %v", err) + } + attached = true + }) + + // Wait for the process to stop + var status unix.WaitStatus + _, err = unix.Wait4(pid, &status, 0, nil) + if err != nil { + t.Fatalf("Wait4 failed: %v", err) + } + if !status.Stopped() { + t.Fatalf("Process not stopped after attach, status: %v", status) + } + + // Test PtraceGetRegs + t.Run("GetRegs", func(t *testing.T) { + var regs unix.PtraceRegs + err := unix.PtraceGetRegs(pid, ®s) + if err != nil { + t.Fatalf("PtraceGetRegs failed: %v", err) + } + + // Verify PSW address is reasonable (not zero, not too low) + pswAddr := regs.Psw.Addr + if pswAddr < 0x1000 { + t.Errorf("PSW address too low: 0x%x", pswAddr) + } + + // Verify at least some GPRs are non-zero + nonZeroCount := 0 + highBitsSet := 0 + for i, gpr := range regs.Gprs { + if gpr != 0 { + nonZeroCount++ + } + if (gpr >> 32) != 0 { + highBitsSet++ + } + t.Logf("GPR[%d] = 0x%016x (high: 0x%08x, low: 0x%08x)", i, gpr, uint32(gpr>>32), uint32(gpr)) + } + if nonZeroCount == 0 { + t.Error("All GPRs are zero, which is unlikely") + } + + // Check PSW format and determine AMODE + // z/OS ptrace returns ESA/390 format, but PtraceGetRegs converts it to z/Architecture format. + // After conversion: + // - Bit 12 (ECMODE31BIT) should be 0 (z/Architecture format) + // - AMODE is in bits 31-32 of PSW Mask (0x0000000180000000 for AMODE64) + const ECMODE31BIT = 0x0000000000080000 + const AMODE31BIT_MASK = 0x0000000080000000 // Bit 32 of mask = AMODE 31 + const AMODE64BIT_MASK = 0x0000000180000000 // Bits 31-32 of mask = EA+BA = AMODE 64 + + // Verify we got z/Architecture format (bit 12 should be clear after conversion) + if (regs.Psw.Mask & ECMODE31BIT) != 0 { + t.Logf("WARNING: Expected z/Architecture format PSW (bit 12=0) after conversion") + } + + // Check AMODE from bits 31-32 of PSW Mask + isAmode64 := (regs.Psw.Mask & AMODE64BIT_MASK) == AMODE64BIT_MASK + isAmode31 := (regs.Psw.Mask & AMODE31BIT_MASK) == AMODE31BIT_MASK + t.Logf("PSW Format: z/Architecture (converted from ESA/390), AMODE 64: %v, AMODE 31: %v (Mask bits 31-32 = 0x%x)", + isAmode64, isAmode31, (regs.Psw.Mask >> 32) & 0x3) + t.Logf("High bits set in %d registers", highBitsSet) + + if isAmode64 && highBitsSet == 0 { + t.Logf("WARNING: AMODE 64 program but no high bits set in any register - PT_READ_GPRH may not be working") + } + + t.Logf("PSW Mask: 0x%016x", regs.Psw.Mask) + t.Logf("PSW Addr: 0x%016x (31-bit: 0x%08x)", regs.Psw.Addr, pswAddr) + + // Dump memory around PSW address to verify it points to real code + pc := regs.Psw.Addr + if !isAmode64 { + pc = pc & 0x7FFFFFFF + } + + // Read 64 bytes: 32 bytes before PC and 32 bytes after PC + memBuf := make([]byte, 64) + startAddr := pc - 32 + n, err := unix.PtracePeekText(pid, uintptr(startAddr), memBuf) + if err != nil { + t.Logf("Failed to read memory around PC: %v", err) + } else { + t.Logf("Memory dump around PC (0x%08x):", pc) + t.Logf(" [PC-32 to PC-1] (0x%08x): % 02x", startAddr, memBuf[0:32]) + t.Logf(" [PC to PC+31] (0x%08x): % 02x", pc, memBuf[32:64]) + if n != 64 { + t.Logf(" Warning: Only read %d bytes of 64 requested", n) + } + } + }) + + // Test PtracePeekText + t.Run("PeekText", func(t *testing.T) { + var regs unix.PtraceRegs + err := unix.PtraceGetRegs(pid, ®s) + if err != nil { + t.Fatalf("PtraceGetRegs failed: %v", err) + } + + // Check PSW format and AMODE + + const ECMODE31BIT = 0x0000000000080000 + + const AMODE31BIT_ADDR = 0x0000000080000000 + + const AMODE64BIT_MASK = 0x0000000180000000 + + var isAmode64 bool + + if (regs.Psw.Mask & ECMODE31BIT) != 0 { + + // ESA/390 format: check bit 32 of PSW Addr + + isAmode64 = (regs.Psw.Addr & AMODE31BIT_ADDR) == 0 + + } else { + + // z/Architecture format: check EA+BA bits + + isAmode64 = (regs.Psw.Mask & AMODE64BIT_MASK) == AMODE64BIT_MASK + + } + + pc := regs.Psw.Addr + // In 31-bit mode, the high bit (0x80000000) is the addressing mode indicator + // and must be masked off to get the actual address + if !isAmode64 { + pc = pc & 0x7FFFFFFF + } + + buf := make([]byte, 16) + n, err := unix.PtracePeekText(pid, uintptr(pc), buf) + if err != nil { + t.Fatalf("PtracePeekText failed: %v", err) + } + if n != len(buf) { + t.Errorf("PtracePeekText read %d bytes, expected %d", n, len(buf)) + } + + t.Logf("Instruction bytes at PC 0x%x: % 02x", pc, buf) + }) + + // Test PtracePeekData + t.Run("PeekData", func(t *testing.T) { + var regs unix.PtraceRegs + err := unix.PtraceGetRegs(pid, ®s) + if err != nil { + t.Fatalf("PtraceGetRegs failed: %v", err) + } + + // Use stack pointer (R4) as a data address + sp := regs.Gprs[4] + if sp < 0x1000 { + t.Skip("Stack pointer too low, skipping PeekData test") + } + + buf := make([]byte, 16) + n, err := unix.PtracePeekData(pid, uintptr(sp), buf) + if err != nil { + t.Logf("PeekData failed (may be expected if address unmapped): %v", err) + } else { + if n != len(buf) { + t.Errorf("PtracePeekData read %d bytes, expected %d", n, len(buf)) + } + t.Logf("Data at SP 0x%x: % 02x", sp, buf) + } + }) + + // Test PtracePokeText/PtracePokeData (read-only test, don't actually modify) + t.Run("PokeOperations", func(t *testing.T) { + // We won't actually poke to avoid corrupting the process + // Just verify the functions exist and have correct signatures + var regs unix.PtraceRegs + err := unix.PtraceGetRegs(pid, ®s) + if err != nil { + t.Fatalf("PtraceGetRegs failed: %v", err) + } + + // Check PSW format and AMODE + + const ECMODE31BIT = 0x0000000000080000 + + const AMODE31BIT_ADDR = 0x0000000080000000 + + const AMODE64BIT_MASK = 0x0000000180000000 + + var isAmode64 bool + + if (regs.Psw.Mask & ECMODE31BIT) != 0 { + + // ESA/390 format: check bit 32 of PSW Addr + + isAmode64 = (regs.Psw.Addr & AMODE31BIT_ADDR) == 0 + + } else { + + // z/Architecture format: check EA+BA bits + + isAmode64 = (regs.Psw.Mask & AMODE64BIT_MASK) == AMODE64BIT_MASK + + } + + pc := regs.Psw.Addr + // In 31-bit mode, mask off the addressing mode indicator bit + if !isAmode64 { + pc = pc & 0x7FFFFFFF + } + + // Read original data + origBuf := make([]byte, 4) + n, err := unix.PtracePeekText(pid, uintptr(pc), origBuf) + if err != nil || n != len(origBuf) { + t.Fatalf("Failed to read original data: %v", err) + } + + // Write same data back (no-op modification) + n, err = unix.PtracePokeText(pid, uintptr(pc), origBuf) + if err != nil { + t.Logf("PtracePokeText failed (may be expected for read-only text): %v", err) + } else if n != len(origBuf) { + t.Errorf("PtracePokeText wrote %d bytes, expected %d", n, len(origBuf)) + } + + // Verify data unchanged + verifyBuf := make([]byte, 4) + n, err = unix.PtracePeekText(pid, uintptr(pc), verifyBuf) + if err == nil && n == len(verifyBuf) { + for i := range origBuf { + if origBuf[i] != verifyBuf[i] { + t.Errorf("Data changed at offset %d: 0x%02x -> 0x%02x", i, origBuf[i], verifyBuf[i]) + } + } + } + }) + + // Test PtraceSetRegs (read-modify-write with no actual change) + t.Run("SetRegs", func(t *testing.T) { + var regs unix.PtraceRegs + err := unix.PtraceGetRegs(pid, ®s) + if err != nil { + t.Fatalf("PtraceGetRegs failed: %v", err) + } + + // Save original GPR[15] value + origGPR15 := regs.Gprs[15] + + // Test 1: Write same values back (no-op modification) + err = unix.PtraceSetRegs(pid, ®s) + if err != nil { + t.Fatalf("PtraceSetRegs failed: %v", err) + } + + // Verify values unchanged + var verifyRegs unix.PtraceRegs + err = unix.PtraceGetRegs(pid, &verifyRegs) + if err != nil { + t.Fatalf("PtraceGetRegs (verify) failed: %v", err) + } + + // Compare PSW (mask off addressing mode bit for 31-bit mode) + isAmode64 := (regs.Psw.Mask & 0x0000000180000000) == 0x0000000180000000 + origPswAddr := regs.Psw.Addr + verifyPswAddr := verifyRegs.Psw.Addr + if !isAmode64 { + origPswAddr = origPswAddr & 0x7FFFFFFF + verifyPswAddr = verifyPswAddr & 0x7FFFFFFF + } + if verifyPswAddr != origPswAddr { + t.Errorf("PSW Addr changed: 0x%08x -> 0x%08x", origPswAddr, verifyPswAddr) + } + + // Test 2: Modify GPR[15] to a value >4GB and verify high bits are preserved + testValue := uint64(0x0000005012345678) // High bits: 0x00000050 + regs.Gprs[15] = testValue + + err = unix.PtraceSetRegs(pid, ®s) + if err != nil { + t.Fatalf("PtraceSetRegs (with >4GB value) failed: %v", err) + } + + // Read back and verify + var regs64 unix.PtraceRegs + err = unix.PtraceGetRegs(pid, ®s64) + if err != nil { + t.Fatalf("PtraceGetRegs (verify >4GB) failed: %v", err) + } + + if regs64.Gprs[15] != testValue { + t.Errorf("GPR[15] >4GB value not preserved: wrote 0x%016x, read 0x%016x", + testValue, regs64.Gprs[15]) + t.Errorf(" High 32 bits: wrote 0x%08x, read 0x%08x", + uint32(testValue>>32), uint32(regs64.Gprs[15]>>32)) + } else { + t.Logf("GPR[15] >4GB value preserved: 0x%016x (high: 0x%08x)", + regs64.Gprs[15], uint32(regs64.Gprs[15]>>32)) + } + + // Restore original value + regs.Gprs[15] = origGPR15 + err = unix.PtraceSetRegs(pid, ®s) + if err != nil { + t.Logf("Warning: Failed to restore GPR[15]: %v", err) + } + + t.Logf("PtraceSetRegs successful - all registers preserved, >4GB values work") + }) + + // Test helper functions + t.Run("InstructionHelpers", func(t *testing.T) { + // Test GetInstructionLength + testCases := []struct { + firstByte byte + expected int + }{ + {0x00, 2}, // 00xxxxxx -> 2 bytes + {0x3F, 2}, // 00xxxxxx -> 2 bytes + {0x40, 4}, // 01xxxxxx -> 4 bytes + {0x7F, 4}, // 01xxxxxx -> 4 bytes + {0x80, 4}, // 10xxxxxx -> 4 bytes + {0xBF, 4}, // 10xxxxxx -> 4 bytes + {0xC0, 6}, // 11xxxxxx -> 6 bytes + {0xFF, 6}, // 11xxxxxx -> 6 bytes + } + + for _, tc := range testCases { + length := unix.GetInstructionLength(tc.firstByte) + if length != tc.expected { + t.Errorf("GetInstructionLength(0x%02x) = %d, expected %d", tc.firstByte, length, tc.expected) + } + } + }) + + t.Run("BranchDestination", func(t *testing.T) { + // Test CalculateBranchDest with a simple relative branch + // BRC (Branch Relative on Condition) - A7x4 format + // A7F4 0010 = BRC 15,+16 (unconditional branch forward 16 bytes) + insn := []byte{0xA7, 0xF4, 0x00, 0x10, 0x00, 0x00} + pc := uint64(0x10000) + + dest := unix.CalculateBranchDest(pid, pc, insn) + expected := uint64(pc + (0x10 * 2)) // Displacement is in halfwords + + if dest != expected && dest != unix.NO_BRANCH_DEST { + t.Logf("CalculateBranchDest: got 0x%x, expected 0x%x (may vary based on register values)", dest, expected) + } + + // Test non-branch instruction + nonBranchInsn := []byte{0x18, 0x12, 0x00, 0x00, 0x00, 0x00} // LR R1,R2 + dest = unix.CalculateBranchDest(pid, pc, nonBranchInsn) + if dest != unix.NO_BRANCH_DEST { + t.Errorf("Non-branch instruction returned destination 0x%x, expected NO_BRANCH_DEST", dest) + } + }) + + // Test single-step emulation (while still attached) + t.Run("SingleStep", func(t *testing.T) { + if !attached { + t.Skip("Process not attached") + return + } + + // Helper function to respawn process if it exits + respawnProcess := func() (int, error) { + // Start new test program + newCmd := exec.Command(binFile) + newCmd.Stdout = os.Stdout + newCmd.Env = append(os.Environ(), "STEPLIB=CEE.SCEERUN2") + err := newCmd.Start() + if err != nil { + return 0, fmt.Errorf("failed to start new process: %v", err) + } + newPid := newCmd.Process.Pid + + // Give it time to start + time.Sleep(50 * time.Millisecond) + + // Attach to new process + err = unix.PtraceAttach(newPid) + if err != nil { + newCmd.Process.Kill() + return 0, fmt.Errorf("failed to attach to new process: %v", err) + } + + // Wait for attach to complete + var status unix.WaitStatus + _, err = unix.Wait4(newPid, &status, 0, nil) + if err != nil { + unix.PtraceDetach(newPid) + newCmd.Process.Kill() + return 0, fmt.Errorf("wait after attach failed: %v", err) + } + + return newPid, nil + } + + // Declare status before any goto statements + var status unix.WaitStatus + + // Continue execution briefly to get out of LE/system library code + err := unix.PtraceCont(pid, 0) + if err != nil { + // Process may have already exited - try to respawn + if err == unix.ESRCH { + t.Logf("Process exited, respawning for SingleStep test...") + newPid, respawnErr := respawnProcess() + if respawnErr != nil { + t.Skipf("Could not respawn process: %v", respawnErr) + return + } + pid = newPid + attached = true + t.Logf("Respawned process with PID %d (already stopped and attached)", pid) + + // Respawned process is already stopped after attach, skip to single-step + goto skipContinue + } else { + t.Fatalf("PtraceCont failed: %v", err) + } + } + + // Wait a tiny bit and stop it again + time.Sleep(10 * time.Millisecond) + err = unix.Kill(pid, unix.SIGSTOP) + if err != nil { + // Process may have exited during continuation + if err == unix.ESRCH { + t.Logf("Process exited during continuation, respawning...") + newPid, respawnErr := respawnProcess() + if respawnErr != nil { + t.Skipf("Could not respawn process: %v", respawnErr) + return + } + pid = newPid + attached = true + t.Logf("Respawned process with PID %d (already stopped)", pid) + goto skipContinue + } else { + t.Fatalf("Kill(SIGSTOP) failed: %v", err) + } + } + + // Wait for stop + _, err = unix.Wait4(pid, &status, 0, nil) + if err != nil { + // Process may have exited + if err == unix.ECHILD || err == unix.ESRCH { + t.Logf("Process exited before stop, respawning...") + newPid, respawnErr := respawnProcess() + if respawnErr != nil { + t.Skipf("Could not respawn process: %v", respawnErr) + return + } + pid = newPid + attached = true + t.Logf("Respawned process with PID %d (already stopped)", pid) + goto skipContinue + } else { + t.Fatalf("Wait4 after SIGSTOP failed: %v", err) + } + } + + // Check if process exited instead of stopping + if status.Exited() { + t.Logf("Process exited, respawning for test...") + newPid, respawnErr := respawnProcess() + if respawnErr != nil { + t.Skipf("Could not respawn process: %v", respawnErr) + return + } + pid = newPid + attached = true + t.Logf("Respawned process with PID %d (already stopped)", pid) + } + + skipContinue: + + // Get initial state + var regs1 unix.PtraceRegs + err = unix.PtraceGetRegs(pid, ®s1) + if err != nil { + t.Fatalf("PtraceGetRegs failed: %v", err) + } + + // Check PSW format and AMODE + const ECMODE31BIT = 0x0000000000080000 + const AMODE31BIT_ADDR = 0x0000000080000000 + const AMODE64BIT_MASK = 0x0000000180000000 + var isAmode64 bool + if (regs1.Psw.Mask & ECMODE31BIT) != 0 { + // ESA/390 format: check bit 32 of PSW Addr + isAmode64 = (regs1.Psw.Addr & AMODE31BIT_ADDR) == 0 + } else { + // z/Architecture format: check EA+BA bits + isAmode64 = (regs1.Psw.Mask & AMODE64BIT_MASK) == AMODE64BIT_MASK + } + pc1 := regs1.Psw.Addr + if !isAmode64 { + pc1 = pc1 & 0x7FFFFFFF + } + t.Logf("Initial PC: 0x%08x", pc1) + + // Read instruction at PC + insn := make([]byte, 8) + _, err = unix.PtracePeekText(pid, uintptr(pc1), insn) + if err != nil { + t.Fatalf("PtracePeekText failed: %v", err) + } + t.Logf("Instruction at PC: % 02x", insn[:6]) + + // Calculate expected next addresses + nextPC := unix.GetNextInstructionAddr(pc1, insn) + branchDest := unix.CalculateBranchDest(pid, pc1, insn) + t.Logf("Next PC: 0x%08x, Branch dest: 0x%08x", nextPC, branchDest) + + // Execute single step + err = unix.PtraceSingleStep(pid) + if err != nil { + // Single-step may fail if still in read-only LE/system code + if err == unix.EIO { + t.Skipf("PtraceSingleStep failed - still in read-only LE/system code at 0x%08x: %v", nextPC, err) + return + } + t.Fatalf("PtraceSingleStep failed: %v", err) + } + + // Get new state + var regs2 unix.PtraceRegs + err = unix.PtraceGetRegs(pid, ®s2) + if err != nil { + t.Fatalf("PtraceGetRegs (after step) failed: %v", err) + } + pc2 := regs2.Psw.Addr + if !isAmode64 { + pc2 = pc2 & 0x7FFFFFFF + } + t.Logf("After step PC: 0x%08x", pc2) + + // Verify PC moved to one of the expected addresses + if pc2 != nextPC && pc2 != branchDest { + t.Errorf("PC after step (0x%x) is neither nextPC (0x%x) nor branchDest (0x%x)", + pc2, nextPC, branchDest) + } else { + t.Logf("Single step successful: PC moved from 0x%x to 0x%x", pc1, pc2) + } + + // Test multiple steps + t.Run("MultipleSteps", func(t *testing.T) { + // Check PSW format and AMODE + var regs unix.PtraceRegs + err := unix.PtraceGetRegs(pid, ®s) + if err != nil { + t.Fatalf("PtraceGetRegs failed: %v", err) + } + const ECMODE31BIT = 0x0000000000080000 + const AMODE31BIT_ADDR = 0x0000000080000000 + const AMODE64BIT_MASK = 0x0000000180000000 + var isAmode64 bool + if (regs.Psw.Mask & ECMODE31BIT) != 0 { + // ESA/390 format: check bit 32 of PSW Addr + isAmode64 = (regs.Psw.Addr & AMODE31BIT_ADDR) == 0 + } else { + // z/Architecture format: check EA+BA bits + isAmode64 = (regs.Psw.Mask & AMODE64BIT_MASK) == AMODE64BIT_MASK + } + + prevPC := pc2 + if !isAmode64 { + prevPC = prevPC & 0x7FFFFFFF + } + + for i := 0; i < 5; i++ { + err := unix.PtraceSingleStep(pid) + if err != nil { + t.Fatalf("PtraceSingleStep (iteration %d) failed: %v", i, err) + } + + err = unix.PtraceGetRegs(pid, ®s) + if err != nil { + t.Fatalf("PtraceGetRegs (iteration %d) failed: %v", i, err) + } + + currentPC := regs.Psw.Addr + if !isAmode64 { + currentPC = currentPC & 0x7FFFFFFF + } + if currentPC == prevPC { + t.Errorf("PC did not advance on iteration %d: still at 0x%x", i, currentPC) + } + t.Logf("Step %d: PC = 0x%08x", i+1, currentPC) + prevPC = currentPC + } + }) + }) + + // Test PtraceCont and PtraceDetach + t.Run("ContAndDetach", func(t *testing.T) { + if !attached { + t.Skip("Process not attached") + return + } + + // Detach directly without continuing (process is already stopped) + err := unix.PtraceDetach(pid) + if err != nil && err != unix.EINTR && err != unix.ESRCH { + t.Fatalf("PtraceDetach failed: %v", err) + } + if err == unix.EINTR { + t.Logf("PtraceDetach interrupted (EINTR), but detach likely succeeded") + } + if err == unix.ESRCH { + t.Logf("Process already exited, detach not needed") + } + attached = false + + t.Logf("Successfully detached from process (or process exited)") + }) + + // Test instruction length detection + t.Run("InstructionLength", func(t *testing.T) { + testCases := []struct { + name string + firstByte byte + expected int + }{ + {"2-byte (00)", 0x00, 2}, + {"2-byte (3F)", 0x3F, 2}, + {"4-byte (40)", 0x40, 4}, + {"4-byte (7F)", 0x7F, 4}, + {"4-byte (80)", 0x80, 4}, + {"4-byte (BF)", 0xBF, 4}, + {"6-byte (C0)", 0xC0, 6}, + {"6-byte (FF)", 0xFF, 6}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + length := unix.GetInstructionLength(tc.firstByte) + if length != tc.expected { + t.Errorf("GetInstructionLength(0x%02x) = %d, expected %d", + tc.firstByte, length, tc.expected) + } + }) + } + }) + + // Test unsupported operations (should return ENOSYS) - while still attached + t.Run("UnsupportedOperations", func(t *testing.T) { + if !attached { + t.Skip("Process not attached") + return + } + + // Test operations that should return ENOSYS + err := unix.PtraceSetOptions(pid, 0) + if err != unix.ENOSYS { + t.Errorf("PtraceSetOptions: expected ENOSYS, got %v", err) + } + + _, err = unix.PtraceGetEventMsg(pid) + if err != unix.ENOSYS { + t.Errorf("PtraceGetEventMsg: expected ENOSYS, got %v", err) + } + + err = unix.PtraceInterrupt(pid) + if err != unix.ENOSYS { + t.Errorf("PtraceInterrupt: expected ENOSYS, got %v", err) + } + + // PtracePokeUser should return ENOSYS + _, err = unix.PtracePokeUser(pid, 0, []byte{0}) + if err != unix.ENOSYS { + t.Errorf("PtracePokeUser: expected ENOSYS, got %v", err) + } + }) + + // Test branch destination calculation (while still attached) + t.Run("BranchDestination", func(t *testing.T) { + if !attached { + t.Skip("Process not attached") + return + } + + testCases := []struct { + name string + insn []byte + pc uint64 + wantDest bool // true if should have destination + }{ + { + name: "BRC unconditional forward", + insn: []byte{0xA7, 0xF4, 0x00, 0x10, 0x00, 0x00}, // BRC 15,+16 + pc: 0x10000, + wantDest: true, + }, + { + name: "BRC conditional", + insn: []byte{0xA7, 0x84, 0x00, 0x08, 0x00, 0x00}, // BRC 8,+8 + pc: 0x10000, + wantDest: true, + }, + { + name: "BRC no-op (mask=0)", + insn: []byte{0xA7, 0x04, 0x00, 0x10, 0x00, 0x00}, // BRC 0,+16 + pc: 0x10000, + wantDest: false, + }, + { + name: "Non-branch (LR)", + insn: []byte{0x18, 0x12, 0x00, 0x00, 0x00, 0x00}, // LR R1,R2 + pc: 0x10000, + wantDest: false, + }, + { + name: "BRCL long branch", + insn: []byte{0xC0, 0xF4, 0x00, 0x00, 0x10, 0x00}, // BRCL 15,+0x1000 + pc: 0x10000, + wantDest: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dest := unix.CalculateBranchDest(pid, tc.pc, tc.insn) + + if tc.wantDest { + if dest == unix.NO_BRANCH_DEST { + t.Errorf("Expected branch destination, got NO_BRANCH_DEST") + } else { + t.Logf("Branch destination: 0x%08x", dest) + } + } else { + if dest != unix.NO_BRANCH_DEST { + t.Errorf("Expected NO_BRANCH_DEST, got 0x%08x", dest) + } + } + }) + } + }) + + // Final cleanup: ensure process is killed if still running + if attached { + // Try to detach first + _ = unix.PtraceDetach(pid) } - cmd.Process.Kill() + // Kill the process if it's still alive + _ = unix.Kill(pid, unix.SIGKILL) + // Wait to reap the zombie + _, _ = unix.Wait4(pid, nil, 0, nil) } func TestFutimesat(t *testing.T) { diff --git a/unix/ztypes_zos_s390x.go b/unix/ztypes_zos_s390x.go index 2e5d5a443..1d9d465fe 100644 --- a/unix/ztypes_zos_s390x.go +++ b/unix/ztypes_zos_s390x.go @@ -209,6 +209,85 @@ type TCPInfo struct { Total_retrans uint32 } + +// Ptrace structures for z/OS + +type PtraceRegs struct { + Psw PtracePsw + Gprs [16]uint64 + Acrs [16]uint32 + Orig_gpr2 uint64 + Fp_regs PtraceFpregs + Per_info PtracePer + Ieee_instruction_pointer uint64 +} + +type PtracePsw struct { + Mask uint64 + Addr uint64 +} + +type PtraceFpregs struct { + Fpc uint32 + Fprs [16]float64 +} + +type PtracePer struct { + Control_regs [3]uint64 + _ [8]byte + Starting_addr uint64 + Ending_addr uint64 + Perc_atmid uint16 + Address uint64 + Access_id uint8 + _ [7]byte +} + +// PT_BLOCKREQ structures for block ptrace requests +// Layout matches BPXYPTRC HLASM macro (PtBR_GPR + PtBR_GPR_EXT): +// Offset 0-1: Writebitflags (PtBR_GPR_CntlGPR, 2 bytes) +// Offset 2-3: Wpsw (PtBR_GPR_CntlMisc, 2 bytes with WPSW bit flag) +// Offset 4-15: Reserved (12 bytes) +// Offset 16-79: Gpr (64 bytes, 16 GPRs) +// Offset 80-143: Ctl (64 bytes, 16 CRs) +// Offset 144-151: Psw old format (8 bytes, PtBR_GPR_PSW) +// Offset 152-167: Pswg extended (16 bytes, PtBR_GPR_PSWG for 64-bit) +// Total: 168 bytes (base 152 + extended 16) +type PtraceBlkGpr struct { + Writebitflags uint16 // 2 bytes at offset 0 (PtBR_GPR_CntlGPR) + Wpsw uint16 // 2 bytes at offset 2 (PtBR_GPR_CntlMisc with WPSW flag) + _ [12]byte // 12 bytes reserved (offset 4-15) + Gpr [16]uint32 // 64 bytes at offset 16 (GPRs) + Ctl [16]uint32 // 64 bytes at offset 80 (CRs) + Psw [8]byte // 8 bytes at offset 144 (old PSW format) + Pswg [16]byte // 16 bytes at offset 152 (extended PSWG) + // Total: 2+2+12+64+64+8+16 = 168 bytes +} + +type PtraceBlkReqReq struct { + Reqtype int32 + Reqstat int32 + Reqdata uint32 + _ [4]byte +} + +type PtraceBlkReq struct { + Numreq int32 + _ [12]byte +} + +// PT_READ_U structures for reading user area (control info) +type PtraceBlkUarOcw struct { + Ofs uint32 // Control information offset + Ctl uint32 // Control information +} + +type PtraceBlkUar struct { + Num uint32 // Number of entries + _ [4]byte +} + + type _Gid_t uint32 type rusage_zos struct { From 892ef86f3e76b6357ff8621acc02cf042ae28763 Mon Sep 17 00:00:00 2001 From: Joon Lee Date: Thu, 2 Jul 2026 14:56:51 -0400 Subject: [PATCH 2/6] unix: add new z/OS syscall numbers --- unix/zsysnum_zos_s390x.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/unix/zsysnum_zos_s390x.go b/unix/zsysnum_zos_s390x.go index 5e8c263ca..f09f84478 100644 --- a/unix/zsysnum_zos_s390x.go +++ b/unix/zsysnum_zos_s390x.go @@ -2848,5 +2848,18 @@ const ( SYS_____CHATTRAT64_A = 0xE39 // 3641 SYS_MADVISE = 0xE3A // 3642 SYS___AUTHENTICATE = 0xE3B // 3643 + SYS_GETPWENT_R = 0xE3C // 3644 + SYS___GETPWENT_R_A = 0xE3D // 3645 + SYS_STRNLEN = 0xE3E // 3646 + SYS_STPCPY = 0xE3F // 3647 + SYS_STPNCPY = 0xE40 // 3648 + SYS_STRSIGNAL = 0xE41 // 3649 + SYS_WCPCPY = 0xE42 // 3650 + SYS_WCPNCPY = 0xE43 // 3651 + SYS_WCSNLEN = 0xE44 // 3652 + SYS___STRSIGNAL_A = 0xE45 // 3653 + SYS_FDOPENDIR = 0xE46 // 3654 + SYS_FDCLOSEDIR = 0xE47 // 3655 + SYS_SYSCALL = 0xE48 // 3656 ) From 69529a77fb351a8402e8a468aa338297a4950012 Mon Sep 17 00:00:00 2001 From: Joon Lee Date: Thu, 2 Jul 2026 14:57:06 -0400 Subject: [PATCH 3/6] unix: refactor z/OS assembly to use runtime implementations Move z/OS LE interaction functions from unix package to runtime package in order to reduce code duplication. --- unix/asm_zos_s390x.s | 190 +------------------------------------- unix/syscall_zos_s390x.go | 4 - 2 files changed, 4 insertions(+), 190 deletions(-) diff --git a/unix/asm_zos_s390x.s b/unix/asm_zos_s390x.s index 813dfad7d..980f07a3b 100644 --- a/unix/asm_zos_s390x.s +++ b/unix/asm_zos_s390x.s @@ -30,21 +30,8 @@ #define SVC_LOAD BYTE $0x0A; BYTE $0x08 // SVC 08 LOAD #define SVC_DELETE BYTE $0x0A; BYTE $0x09 // SVC 09 DELETE -DATA zosLibVec<>(SB)/8, $0 -GLOBL zosLibVec<>(SB), NOPTR, $8 - -TEXT ·initZosLibVec(SB), NOSPLIT|NOFRAME, $0-0 - MOVW PSALAA, R8 - MOVD LCA64(R8), R8 - MOVD CAA(R8), R8 - MOVD EDCHPXV(R8), R8 - MOVD R8, zosLibVec<>(SB) - RET - TEXT ·GetZosLibVec(SB), NOSPLIT|NOFRAME, $0-0 - MOVD zosLibVec<>(SB), R8 - MOVD R8, ret+0(FP) - RET + JMP runtime·GetZosLibVec(SB) TEXT ·clearErrno(SB), NOSPLIT, $0-0 BL addrerrno<>(SB) @@ -160,163 +147,13 @@ TEXT ·gettid(SB), NOSPLIT, $0 // errno and errno2 is retrieved // TEXT ·CallLeFuncWithErr(SB), NOSPLIT, $0 - MOVW PSALAA, R8 - MOVD LCA64(R8), R8 - MOVD CAA(R8), R9 - MOVD g, GOCB(R9) - - // Restore LE stack. - MOVD SAVSTACK_ASYNC(R8), R9 // R9-> LE stack frame saving address - MOVD 0(R9), R4 // R4-> restore previously saved stack frame pointer - - MOVD parms_base+8(FP), R7 // R7 -> argument array - MOVD parms_len+16(FP), R8 // R8 number of arguments - - // arg 1 ---> R1 - CMP R8, $0 - BEQ docall - SUB $1, R8 - MOVD 0(R7), R1 - - // arg 2 ---> R2 - CMP R8, $0 - BEQ docall - SUB $1, R8 - ADD $8, R7 - MOVD 0(R7), R2 - - // arg 3 --> R3 - CMP R8, $0 - BEQ docall - SUB $1, R8 - ADD $8, R7 - MOVD 0(R7), R3 - - CMP R8, $0 - BEQ docall - MOVD $2176+16, R6 // starting LE stack address-8 to store 4th argument - -repeat: - ADD $8, R7 - MOVD 0(R7), R0 // advance arg pointer by 8 byte - ADD $8, R6 // advance LE argument address by 8 byte - MOVD R0, (R4)(R6*1) // copy argument from go-slice to le-frame - SUB $1, R8 - CMP R8, $0 - BNE repeat - -docall: - MOVD funcdesc+0(FP), R8 // R8-> function descriptor - LMG 0(R8), R5, R6 - MOVD $0, 0(R9) // R9 address of SAVSTACK_ASYNC - LE_CALL // balr R7, R6 (return #1) - NOPH - MOVD R3, ret+32(FP) - CMP R3, $-1 // compare result to -1 - BNE done - - // retrieve errno and errno2 - MOVD zosLibVec<>(SB), R8 - ADD $(__errno), R8 - LMG 0(R8), R5, R6 - LE_CALL // balr R7, R6 __errno (return #3) - NOPH - MOVWZ 0(R3), R3 - MOVD R3, err+48(FP) - MOVD zosLibVec<>(SB), R8 - ADD $(__err2ad), R8 - LMG 0(R8), R5, R6 - LE_CALL // balr R7, R6 __err2ad (return #2) - NOPH - MOVW (R3), R2 // retrieve errno2 - MOVD R2, errno2+40(FP) // store in return area - -done: - MOVD R4, 0(R9) // Save stack pointer. - RET - + JMP runtime·CallLeFuncWithErr(SB) // // Call LE function, if the return is 0 // errno and errno2 is retrieved // TEXT ·CallLeFuncWithPtrReturn(SB), NOSPLIT, $0 - MOVW PSALAA, R8 - MOVD LCA64(R8), R8 - MOVD CAA(R8), R9 - MOVD g, GOCB(R9) - - // Restore LE stack. - MOVD SAVSTACK_ASYNC(R8), R9 // R9-> LE stack frame saving address - MOVD 0(R9), R4 // R4-> restore previously saved stack frame pointer - - MOVD parms_base+8(FP), R7 // R7 -> argument array - MOVD parms_len+16(FP), R8 // R8 number of arguments - - // arg 1 ---> R1 - CMP R8, $0 - BEQ docall - SUB $1, R8 - MOVD 0(R7), R1 - - // arg 2 ---> R2 - CMP R8, $0 - BEQ docall - SUB $1, R8 - ADD $8, R7 - MOVD 0(R7), R2 - - // arg 3 --> R3 - CMP R8, $0 - BEQ docall - SUB $1, R8 - ADD $8, R7 - MOVD 0(R7), R3 - - CMP R8, $0 - BEQ docall - MOVD $2176+16, R6 // starting LE stack address-8 to store 4th argument - -repeat: - ADD $8, R7 - MOVD 0(R7), R0 // advance arg pointer by 8 byte - ADD $8, R6 // advance LE argument address by 8 byte - MOVD R0, (R4)(R6*1) // copy argument from go-slice to le-frame - SUB $1, R8 - CMP R8, $0 - BNE repeat - -docall: - MOVD funcdesc+0(FP), R8 // R8-> function descriptor - LMG 0(R8), R5, R6 - MOVD $0, 0(R9) // R9 address of SAVSTACK_ASYNC - LE_CALL // balr R7, R6 (return #1) - NOPH - MOVD R3, ret+32(FP) - CMP R3, $0 // compare result to 0 - BNE done - - // retrieve errno and errno2 - MOVD zosLibVec<>(SB), R8 - ADD $(__errno), R8 - LMG 0(R8), R5, R6 - LE_CALL // balr R7, R6 __errno (return #3) - NOPH - MOVWZ 0(R3), R3 - MOVD R3, err+48(FP) - MOVD zosLibVec<>(SB), R8 - ADD $(__err2ad), R8 - LMG 0(R8), R5, R6 - LE_CALL // balr R7, R6 __err2ad (return #2) - NOPH - MOVW (R3), R2 // retrieve errno2 - MOVD R2, errno2+40(FP) // store in return area - XOR R2, R2 - MOVWZ R2, (R3) // clear errno2 - -done: - MOVD R4, 0(R9) // Save stack pointer. - RET - + JMP runtime·CallLeFuncWithPtrReturn(SB) // // function to test if a pointer can be safely dereferenced (content read) // return 0 for succces @@ -360,23 +197,4 @@ TEXT ·ptrtest(SB), NOSPLIT, $0-16 // // func safeload(ptr uintptr) ( value uintptr, error uintptr) TEXT ·safeload(SB), NOSPLIT, $0-24 - MOVD ptr+0(FP), R10 // test pointer in R10 - MOVD $0x0, R6 - BYTE $0xE3; BYTE $0x20; BYTE $0x04; BYTE $0xB8; BYTE $0x00; BYTE $0x17 // llgt 2,1208 - BYTE $0xB9; BYTE $0x17; BYTE $0x00; BYTE $0x22 // llgtr 2,2 - BYTE $0xA5; BYTE $0x26; BYTE $0x7F; BYTE $0xFF // nilh 2,32767 - BYTE $0xE3; BYTE $0x22; BYTE $0x00; BYTE $0x58; BYTE $0x00; BYTE $0x04 // lg 2,88(2) - BYTE $0xE3; BYTE $0x22; BYTE $0x00; BYTE $0x08; BYTE $0x00; BYTE $0x04 // lg 2,8(2) - BYTE $0x41; BYTE $0x22; BYTE $0x03; BYTE $0x68 // la 2,872(2) - BYTE $0xB9; BYTE $0x82; BYTE $0x00; BYTE $0x33 // xgr 3,3 - BYTE $0xA7; BYTE $0x55; BYTE $0x00; BYTE $0x04 // bras 5,lbl1 - BYTE $0xA7; BYTE $0x39; BYTE $0x00; BYTE $0x01 // lghi 3,1 - BYTE $0xB9; BYTE $0x02; BYTE $0x00; BYTE $0x33 // lbl1 ltgr 3,3 - BYTE $0xA7; BYTE $0x74; BYTE $0x00; BYTE $0x08 // brc b'0111',lbl2 - BYTE $0xE3; BYTE $0x52; BYTE $0x00; BYTE $0x00; BYTE $0x00; BYTE $0x24 // stg 5,0(2) - BYTE $0xE3; BYTE $0x6A; BYTE $0x00; BYTE $0x00; BYTE $0x00; BYTE $0x04 // lg 6,0(10) - BYTE $0xB9; BYTE $0x82; BYTE $0x00; BYTE $0x99 // lbl2 xgr 9,9 - BYTE $0xE3; BYTE $0x92; BYTE $0x00; BYTE $0x00; BYTE $0x00; BYTE $0x24 // stg 9,0(2) - MOVD R6, value+8(FP) // result in R6 - MOVD R3, error+16(FP) // error in R3 - RET + JMP runtime·ZosSafeLoad(SB) diff --git a/unix/syscall_zos_s390x.go b/unix/syscall_zos_s390x.go index 7bf5c04bb..edabb6892 100644 --- a/unix/syscall_zos_s390x.go +++ b/unix/syscall_zos_s390x.go @@ -27,14 +27,10 @@ import ( "unsafe" ) -//go:noescape -func initZosLibVec() - //go:noescape func GetZosLibVec() uintptr func init() { - initZosLibVec() r0, _, _ := CallLeFuncWithPtrReturn(GetZosLibVec()+SYS_____GETENV_A<<4, uintptr(unsafe.Pointer(&([]byte("__ZOS_XSYSTRACE\x00"))[0]))) if r0 != 0 { n, _, _ := CallLeFuncWithPtrReturn(GetZosLibVec()+SYS___ATOI_A<<4, r0) From 63d45cb7b35730db797dd0e12b64d59870a34797 Mon Sep 17 00:00:00 2001 From: Joon Lee Date: Thu, 2 Jul 2026 15:01:09 -0400 Subject: [PATCH 4/6] unix: optimize z/OS BPX call register usage - Changes bpxcall to use R15 instead of R9, reducing register pressure. --- unix/bpxsvc_zos.s | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/unix/bpxsvc_zos.s b/unix/bpxsvc_zos.s index 4bd4a1798..1236e1e90 100644 --- a/unix/bpxsvc_zos.s +++ b/unix/bpxsvc_zos.s @@ -20,15 +20,15 @@ TEXT ·bpxcall(SB), NOSPLIT|NOFRAME, $0 MOVD plist_base+0(FP), R1 // r1 points to plist MOVD bpx_offset+24(FP), R2 // r2 offset to BPX vector table MOVD R14, R7 // save r14 - MOVD R15, R8 // save r15 - MOVWZ 16(R0), R9 - MOVWZ 544(R9), R9 - MOVWZ 24(R9), R9 // call vector in r9 - ADD R2, R9 // add offset to vector table - MOVWZ (R9), R9 // r9 points to entry point - BYTE $0x0D // BL R14,R9 --> basr r14,r9 - BYTE $0xE9 // clobbers 0,1,14,15 - MOVD R8, R15 // restore 15 + MOVD R15, R9 // save r15 + MOVWZ 16(R0), R15 + MOVWZ 544(R15), R15 + MOVWZ 24(R15), R15 // call vector in r15 + ADD R2, R15 // add offset to vector table + MOVWZ (R15), R15 // r15 points to entry point + BYTE $0x0D // BL R14,R15 --> basr r14,r15 + BYTE $0xEF // clobbers 0,1,14,15 + MOVD R9, R15 // restore 15 JMP R7 // return via saved return address // func A2e(arr [] byte) From 2723acaf1f4d82f02b05288481197e28f7de50fd Mon Sep 17 00:00:00 2001 From: Joon Lee Date: Mon, 24 Aug 2026 14:14:43 -0400 Subject: [PATCH 5/6] unix: fix unsafe.Pointer in ptrace for z/OS and run gofmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ptrace(request, pid, addr, data uintptr) was passing data to ptracePtr as unsafe.Pointer(data). data carries integer values (signal numbers, register indices, byte counts, target-process addresses) — not Go-managed pointers. Converting an arbitrary uintptr to unsafe.Pointer exposes a synthetic pointer to the garbage collector, which can cause it to crash. Bpx4ptr builds a parms array where parms[3] = &data (pointer to the local copy of the data argument), and BPX4PTR dereferences parms[3] to read the value. The original code stored unsafe.Pointer(data_int) into that local, so BPX4PTR correctly received data_int — but the GC could scan the parms array and mistake data_int for a live heap pointer. Fix by inlining the parms array construction and placing &data at parms[3] directly. BPX4PTR still dereferences parms[3] and obtains the original data integer, but the GC now only sees a valid stack pointer. Also ran `gofmt -w` on all z/OS source files (ptrace_zos.go, ptrace_zos_singlestep.go, syscall_zos_test.go, ztypes_zos_s390x.go) which were not properly formatted. Additionally, copyright year for ptrace_zos.go is now up to date. --- unix/ptrace_zos.go | 134 ++++++++++++++++++++-------------- unix/ptrace_zos_singlestep.go | 6 +- unix/syscall_zos_test.go | 90 +++++++++++------------ unix/ztypes_zos_s390x.go | 31 ++++---- 4 files changed, 143 insertions(+), 118 deletions(-) diff --git a/unix/ptrace_zos.go b/unix/ptrace_zos.go index 2e385cf18..0c14c2fc9 100644 --- a/unix/ptrace_zos.go +++ b/unix/ptrace_zos.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Go Authors. All rights reserved. +// Copyright 2026 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -55,13 +55,40 @@ import "unsafe" // ptrace is the basic wrapper for BPX4PTR on z/OS. // Note: z/OS ptrace (BPX4PTR) takes 5 parameters vs 4 on Linux/Darwin. // The 5th parameter (buffer) is used for operations like PT_READ_BLOCK/PT_WRITE_BLOCK. +// +// data is an integer-valued argument (signal number, register index, byte count, +// or a target-process address). It is not a Go-managed pointer. We must not pass +// it to Bpx4ptr as unsafe.Pointer(data) because the garbage collector would treat +// that integer as a pointer and may crash trying to follow it. +// +// Instead we inline the parms array that Bpx4ptr would construct, placing &data +// at parms[3]. BPX4PTR dereferences parms[3] to obtain the data value, so it +// receives data unchanged while the GC only ever sees a valid stack pointer. func ptrace(request int, pid int, addr uintptr, data uintptr) (err error) { - return ptracePtr(request, pid, addr, unsafe.Pointer(data)) + req32 := int32(request) + pid32 := int32(pid) + var rv, rc, rn int32 + var buffer unsafe.Pointer + parms := [8]unsafe.Pointer{ + unsafe.Pointer(&req32), + unsafe.Pointer(&pid32), + unsafe.Pointer(&addr), + unsafe.Pointer(&data), // &data, not unsafe.Pointer(data) + unsafe.Pointer(&buffer), + unsafe.Pointer(&rv), + unsafe.Pointer(&rc), + unsafe.Pointer(&rn), + } + bpxcall(parms[:], BPX4PTR) + if rv != 0 { + err = errnoErr2(Errno(rc), uintptr(rn)) + } + return } // ptracePtr is a variant that accepts unsafe.Pointer for the data parameter. func ptracePtr(request int, pid int, addr uintptr, data unsafe.Pointer) (err error) { - rv, rc, rn := Bpx4ptr(int32(request), int32(pid), + rv, rc, rn := Bpx4ptr(int32(request), int32(pid), unsafe.Pointer(addr), data, nil) if rv != 0 { err = errnoErr2(Errno(rc), uintptr(rn)) @@ -71,7 +98,7 @@ func ptracePtr(request int, pid int, addr uintptr, data unsafe.Pointer) (err err // ptracePtrWithBuffer is used for operations that require the buffer parameter. func ptracePtrWithBuffer(request int, pid int, addr uintptr, data unsafe.Pointer, buffer unsafe.Pointer) (err error) { - rv, rc, rn := Bpx4ptr(int32(request), int32(pid), + rv, rc, rn := Bpx4ptr(int32(request), int32(pid), unsafe.Pointer(addr), data, buffer) if rv != 0 { err = errnoErr2(Errno(rc), uintptr(rn)) @@ -111,7 +138,7 @@ func ptracePeek(req int, pid int, addr uintptr, out []byte) (count int, err erro if len(out) == 0 { return 0, nil } - + // z/OS PT_READ_BLOCK can read blocks directly without word-by-word alignment // The buffer parameter points to the output buffer buffer := unsafe.Pointer(&out[0]) @@ -139,7 +166,7 @@ func PtracePeekUser(pid int, addr uintptr, out []byte) (count int, err error) { if len(out) == 0 { return 0, nil } - + // PT_READ_U reads 32-bit words from user area // We need to read word-by-word to handle arbitrary lengths n := 0 @@ -149,7 +176,7 @@ func PtracePeekUser(pid int, addr uintptr, out []byte) (count int, err error) { if err != nil { return n, err } - + // Copy up to 4 bytes to output remaining := len(out) - n if remaining >= 4 { @@ -174,7 +201,7 @@ func ptracePoke(pokeReq int, peekReq int, pid int, addr uintptr, data []byte) (c if len(data) == 0 { return 0, nil } - + // z/OS PT_WRITE_BLOCK can write blocks directly buffer := unsafe.Pointer(&data[0]) err = ptracePtrWithBuffer(pokeReq, pid, addr, unsafe.Pointer(uintptr(len(data))), buffer) @@ -214,11 +241,11 @@ func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) { _ = rv _ = rc _ = rn - + // Use PT_BLOCKREQ to read GPRs and PSW in one call // dbx uses 3 requests: PT_READ_GPR, PT_READ_U, PT_READ_GPRH // PT_READ_U is required for ptrace to populate PSW in GPR block - // + // // IMPORTANT: z/OS ptrace always returns 64-bit ESA/390 format PSW in the Psw field. // The 128-bit Pswg field is never populated, even with PSWG_Req bit set. // We convert ESA/390 to z/Architecture format in software (matching DBX behavior). @@ -229,40 +256,40 @@ func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) { uareaSize = int(unsafe.Sizeof(PtraceBlkUar{})) + 6*int(unsafe.Sizeof(PtraceBlkUarOcw{})) totalSize = int(unsafe.Sizeof(PtraceBlkReq{})) + numRequests*reqSize + gprSize*2 + uareaSize ) - + // Allocate buffer for block request buf := make([]byte, totalSize) - + // Setup block request header blkReq := (*PtraceBlkReq)(unsafe.Pointer(&buf[0])) blkReq.Numreq = numRequests - + // Setup request array reqOffset := int(unsafe.Sizeof(PtraceBlkReq{})) reqs := (*[3]PtraceBlkReqReq)(unsafe.Pointer(&buf[reqOffset])) - + // Setup GPR block (lower 32 bits) gprOffset := reqOffset + numRequests*reqSize gprBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprOffset])) - + // Setup user area block (required for PSW) uareaOffset := gprOffset + gprSize uareaBlock := (*PtraceBlkUar)(unsafe.Pointer(&buf[uareaOffset])) uareaBlock.Num = 6 // Number of control info entries - + // Setup user area offset/control words - uareaOcw := (*[6]PtraceBlkUarOcw)(unsafe.Pointer(&buf[uareaOffset + int(unsafe.Sizeof(PtraceBlkUar{}))])) + uareaOcw := (*[6]PtraceBlkUarOcw)(unsafe.Pointer(&buf[uareaOffset+int(unsafe.Sizeof(PtraceBlkUar{}))])) uareaOcw[0].Ofs = 1025 // program interrupt code uareaOcw[1].Ofs = 1026 // abend completion code uareaOcw[2].Ofs = 1027 // abend reason code uareaOcw[3].Ofs = 1028 // signal code uareaOcw[4].Ofs = 1029 // instruction length code uareaOcw[5].Ofs = 1030 // process flags - + // Setup high GPR block (upper 32 bits) gprHighOffset := uareaOffset + uareaSize gprHighBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprHighOffset])) - + // Configure requests (order matters: GPR, U, GPRH) // reqdata contains OFFSET from buffer start, not absolute address // dbx: brr[0].reqdata=(unsigned long)brg; where brg is the offset @@ -272,21 +299,21 @@ func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) { reqs[1].Reqdata = uint32(uareaOffset) reqs[2].Reqtype = PT_READ_GPRH reqs[2].Reqdata = uint32(gprHighOffset) - + // Execute block request - rvBlk, rcBlk, rnBlk := Bpx4ptr(int32(PT_BLOCKREQ), int32(pid), + rvBlk, rcBlk, rnBlk := Bpx4ptr(int32(PT_BLOCKREQ), int32(pid), unsafe.Pointer(&buf[0]), unsafe.Pointer(uintptr(totalSize)), nil) if rvBlk == -1 { return errnoErr2(Errno(rcBlk), uintptr(rnBlk)) } - + // Extract GPRs (combine low and high 32 bits) for i := 0; i < 16; i++ { low := uint64(gprBlock.Gpr[i]) high := uint64(gprHighBlock.Gpr[i]) << 32 regsout.Gprs[i] = high | low } - + // Extract PSW from GPR block structure // z/OS ptrace ALWAYS returns 8-byte ESA/390 format PSW at offset 144. // The 16-byte PSWG field at offset 152 is NEVER populated (always zero). @@ -299,36 +326,36 @@ func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) { // z/Architecture PSW format (16 bytes): // Mask (bytes 0-7): Extended mask with bit 12 = 0, bits 31-32 = EA+BA (AMODE) // Addr (bytes 8-15): Full 64-bit instruction address - + // Read ESA/390 PSW as two 32-bit words - pswWord1 := uint32(gprBlock.Psw[0])<<24 | uint32(gprBlock.Psw[1])<<16 | - uint32(gprBlock.Psw[2])<<8 | uint32(gprBlock.Psw[3]) - pswWord2 := uint32(gprBlock.Psw[4])<<24 | uint32(gprBlock.Psw[5])<<16 | - uint32(gprBlock.Psw[6])<<8 | uint32(gprBlock.Psw[7]) - + pswWord1 := uint32(gprBlock.Psw[0])<<24 | uint32(gprBlock.Psw[1])<<16 | + uint32(gprBlock.Psw[2])<<8 | uint32(gprBlock.Psw[3]) + pswWord2 := uint32(gprBlock.Psw[4])<<24 | uint32(gprBlock.Psw[5])<<16 | + uint32(gprBlock.Psw[6])<<8 | uint32(gprBlock.Psw[7]) + // Convert ESA/390 to z/Architecture format (matching DBX psw_ESA390_to_zArchitecture) const ECMODE31BIT = uint64(0x0000000000080000) const AMODE31BIT = uint64(0x0000000080000000) const ADDR_MASK_31 = uint64(0x000000007FFFFFFF) - + // Extract AMODE bit from address word (bit 0 of word 2) amode31 := uint64(pswWord2) & AMODE31BIT - + // Mask off AMODE bit from address addr := uint64(pswWord2) & ADDR_MASK_31 - + // Convert mask: clear ECMODE31BIT, shift left 32 bits, add AMODE bit mask := uint64(pswWord1) & ^ECMODE31BIT mask = (mask << 32) | amode31 - + regsout.Psw.Mask = mask regsout.Psw.Addr = addr - + // Note: Access registers (Acrs), floating point registers (Fp_regs), // PER info (Per_info), and other fields would require additional // PT_READ_* calls. For now, we focus on GPRs and PSW which are // the most commonly used registers for debugging. - + return nil } @@ -337,9 +364,9 @@ func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) { // Based on ztrace's write_psw function. func ptraceWritePSW(pid int, pswAddr uint32) error { const PTRACE_REG_PSWA = 41 - rv, rc, rn := Bpx4ptr(int32(PT_WRITE_GPR), int32(pid), - unsafe.Pointer(uintptr(PTRACE_REG_PSWA)), - unsafe.Pointer(uintptr(pswAddr)), + rv, rc, rn := Bpx4ptr(int32(PT_WRITE_GPR), int32(pid), + unsafe.Pointer(uintptr(PTRACE_REG_PSWA)), + unsafe.Pointer(uintptr(pswAddr)), nil) if rv == -1 { return errnoErr2(Errno(rc), uintptr(rn)) @@ -357,7 +384,7 @@ func PtraceSetRegs(pid int, regs *PtraceRegs) (err error) { _ = rv _ = rc _ = rn - + // Use PT_BLOCKREQ to write GPRs and PSW in one call const ( numRequests = 2 @@ -365,36 +392,36 @@ func PtraceSetRegs(pid int, regs *PtraceRegs) (err error) { gprSize = int(unsafe.Sizeof(PtraceBlkGpr{})) totalSize = int(unsafe.Sizeof(PtraceBlkReq{})) + numRequests*reqSize + gprSize*2 ) - + // Allocate buffer for block request buf := make([]byte, totalSize) - + // Setup block request header blkReq := (*PtraceBlkReq)(unsafe.Pointer(&buf[0])) blkReq.Numreq = numRequests - + // Setup request array reqOffset := int(unsafe.Sizeof(PtraceBlkReq{})) reqs := (*[2]PtraceBlkReqReq)(unsafe.Pointer(&buf[reqOffset])) - + // Setup GPR block (lower 32 bits) gprOffset := reqOffset + numRequests*reqSize gprBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprOffset])) - + // Setup high GPR block (upper 32 bits) gprHighOffset := gprOffset + gprSize gprHighBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprHighOffset])) - + // Mark all GPRs as modified (set all 16 bits) gprBlock.Writebitflags = 0xFFFF gprHighBlock.Writebitflags = 0xFFFF - + // Split 64-bit GPRs into low and high 32 bits for i := 0; i < 16; i++ { gprBlock.Gpr[i] = uint32(regs.Gprs[i] & 0xFFFFFFFF) gprHighBlock.Gpr[i] = uint32(regs.Gprs[i] >> 32) } - + // Pack PSW for 31-bit targets (current z/OS) // Upper 32 bits = mask, lower 32 bits = address gprBlock.Wpsw = 1 // Mark PSW as modified @@ -410,26 +437,26 @@ func PtraceSetRegs(pid int, regs *PtraceRegs) (err error) { gprBlock.Psw[5] = byte(pswAddr >> 16) gprBlock.Psw[6] = byte(pswAddr >> 8) gprBlock.Psw[7] = byte(pswAddr) - + // Clear extended PSWG (not used for 31-bit targets) for i := 0; i < 16; i++ { gprBlock.Pswg[i] = 0 } - + // Configure requests // reqdata contains OFFSET from buffer start, not absolute address reqs[0].Reqtype = PT_WRITE_GPR reqs[0].Reqdata = uint32(gprOffset) reqs[1].Reqtype = PT_WRITE_GPRH reqs[1].Reqdata = uint32(gprHighOffset) - + // Execute block request - rvBlk, rcBlk, rnBlk := Bpx4ptr(int32(PT_BLOCKREQ), int32(pid), + rvBlk, rcBlk, rnBlk := Bpx4ptr(int32(PT_BLOCKREQ), int32(pid), unsafe.Pointer(&buf[0]), unsafe.Pointer(uintptr(totalSize)), nil) if rvBlk == -1 { return errnoErr2(Errno(rcBlk), uintptr(rnBlk)) } - + return nil } @@ -450,12 +477,11 @@ func PtraceGetEventMsg(pid int) (msg uint, err error) { // PtraceSyscall continues execution and stops at the next syscall entry/exit. // DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_SYSCALL equivalent. // This function falls back to PT_CONTINUE and will NOT stop at syscalls. -// +// // To trace syscalls on z/OS, applications must: // 1. Set breakpoints at BPX syscall entry points (requires knowledge of syscall vector) // 2. Detect BASR instructions that call into the BPX syscall vector // 3. Use PT_LDINFO to identify loaded modules and their entry points -// func PtraceSyscall(pid int, signal int) (err error) { // z/OS doesn't have a direct PTRACE_SYSCALL equivalent // Use PT_CONTINUE as a fallback (won't stop at syscalls) diff --git a/unix/ptrace_zos_singlestep.go b/unix/ptrace_zos_singlestep.go index 7b44d3b95..7d7e2c8d3 100644 --- a/unix/ptrace_zos_singlestep.go +++ b/unix/ptrace_zos_singlestep.go @@ -62,7 +62,7 @@ type tempBreakpoint struct { // singleStepState manages temporary breakpoints for a process type singleStepState struct { - mu sync.Mutex + mu sync.Mutex breakpoints [MAX_TEMP_BREAKPOINTS]tempBreakpoint } @@ -407,7 +407,7 @@ func PtraceSingleStep(pid int) error { // Use direct PSW write like ztrace does, not full register set hitAddr := regs.Psw.Addr if state.isTempBreakpoint(hitAddr - 2) { - if err := ptraceWritePSW(pid, uint32(hitAddr - 2)); err != nil { + if err := ptraceWritePSW(pid, uint32(hitAddr-2)); err != nil { return err } } @@ -422,4 +422,4 @@ func PtraceDetachWithCleanup(pid int) error { state.removeTempBreakpoints(pid) cleanupProcessState(pid) return PtraceDetach(pid) -} \ No newline at end of file +} diff --git a/unix/syscall_zos_test.go b/unix/syscall_zos_test.go index 341c1810c..095b1a3a1 100644 --- a/unix/syscall_zos_test.go +++ b/unix/syscall_zos_test.go @@ -3289,19 +3289,19 @@ int main() { tmpDir := t.TempDir() srcFile := tmpDir + "/ptrace_test.c" binFile := tmpDir + "/ptrace_test" - + err := os.WriteFile(srcFile, []byte(testProg), 0644) if err != nil { t.Fatalf("Failed to write test program: %v", err) } - + // Compile test program as 64-bit to test full 64-bit address support compileCmd := exec.Command("xlc", "-q64", "-o", binFile, srcFile) compileOut, err := compileCmd.CombinedOutput() if err != nil { t.Fatalf("Failed to compile test program: %v\nOutput: %s", err, compileOut) } - + // Verify the executable is actually AMODE 64 fileCmd := exec.Command("/bin/file", binFile) fileOut, err := fileCmd.CombinedOutput() @@ -3310,7 +3310,7 @@ int main() { } else { t.Logf("Executable type: %s", string(fileOut)) } - + // Start the test program with STEPLIB set for 64-bit runtime libraries cmd := exec.Command(binFile) cmd.Stdout = os.Stdout @@ -3324,7 +3324,7 @@ int main() { pid := cmd.Process.Pid attached := false - + // Give the process a moment to start executing user code time.Sleep(50 * time.Millisecond) @@ -3376,41 +3376,41 @@ int main() { if nonZeroCount == 0 { t.Error("All GPRs are zero, which is unlikely") } - + // Check PSW format and determine AMODE // z/OS ptrace returns ESA/390 format, but PtraceGetRegs converts it to z/Architecture format. // After conversion: // - Bit 12 (ECMODE31BIT) should be 0 (z/Architecture format) // - AMODE is in bits 31-32 of PSW Mask (0x0000000180000000 for AMODE64) const ECMODE31BIT = 0x0000000000080000 - const AMODE31BIT_MASK = 0x0000000080000000 // Bit 32 of mask = AMODE 31 - const AMODE64BIT_MASK = 0x0000000180000000 // Bits 31-32 of mask = EA+BA = AMODE 64 - + const AMODE31BIT_MASK = 0x0000000080000000 // Bit 32 of mask = AMODE 31 + const AMODE64BIT_MASK = 0x0000000180000000 // Bits 31-32 of mask = EA+BA = AMODE 64 + // Verify we got z/Architecture format (bit 12 should be clear after conversion) if (regs.Psw.Mask & ECMODE31BIT) != 0 { t.Logf("WARNING: Expected z/Architecture format PSW (bit 12=0) after conversion") } - + // Check AMODE from bits 31-32 of PSW Mask isAmode64 := (regs.Psw.Mask & AMODE64BIT_MASK) == AMODE64BIT_MASK isAmode31 := (regs.Psw.Mask & AMODE31BIT_MASK) == AMODE31BIT_MASK - t.Logf("PSW Format: z/Architecture (converted from ESA/390), AMODE 64: %v, AMODE 31: %v (Mask bits 31-32 = 0x%x)", - isAmode64, isAmode31, (regs.Psw.Mask >> 32) & 0x3) + t.Logf("PSW Format: z/Architecture (converted from ESA/390), AMODE 64: %v, AMODE 31: %v (Mask bits 31-32 = 0x%x)", + isAmode64, isAmode31, (regs.Psw.Mask>>32)&0x3) t.Logf("High bits set in %d registers", highBitsSet) - + if isAmode64 && highBitsSet == 0 { t.Logf("WARNING: AMODE 64 program but no high bits set in any register - PT_READ_GPRH may not be working") } t.Logf("PSW Mask: 0x%016x", regs.Psw.Mask) t.Logf("PSW Addr: 0x%016x (31-bit: 0x%08x)", regs.Psw.Addr, pswAddr) - + // Dump memory around PSW address to verify it points to real code pc := regs.Psw.Addr if !isAmode64 { pc = pc & 0x7FFFFFFF } - + // Read 64 bytes: 32 bytes before PC and 32 bytes after PC memBuf := make([]byte, 64) startAddr := pc - 32 @@ -3458,14 +3458,14 @@ int main() { isAmode64 = (regs.Psw.Mask & AMODE64BIT_MASK) == AMODE64BIT_MASK } - + pc := regs.Psw.Addr // In 31-bit mode, the high bit (0x80000000) is the addressing mode indicator // and must be masked off to get the actual address if !isAmode64 { pc = pc & 0x7FFFFFFF } - + buf := make([]byte, 16) n, err := unix.PtracePeekText(pid, uintptr(pc), buf) if err != nil { @@ -3537,13 +3537,13 @@ int main() { isAmode64 = (regs.Psw.Mask & AMODE64BIT_MASK) == AMODE64BIT_MASK } - + pc := regs.Psw.Addr // In 31-bit mode, mask off the addressing mode indicator bit if !isAmode64 { pc = pc & 0x7FFFFFFF } - + // Read original data origBuf := make([]byte, 4) n, err := unix.PtracePeekText(pid, uintptr(pc), origBuf) @@ -3581,7 +3581,7 @@ int main() { // Save original GPR[15] value origGPR15 := regs.Gprs[15] - + // Test 1: Write same values back (no-op modification) err = unix.PtraceSetRegs(pid, ®s) if err != nil { @@ -3606,40 +3606,40 @@ int main() { if verifyPswAddr != origPswAddr { t.Errorf("PSW Addr changed: 0x%08x -> 0x%08x", origPswAddr, verifyPswAddr) } - + // Test 2: Modify GPR[15] to a value >4GB and verify high bits are preserved testValue := uint64(0x0000005012345678) // High bits: 0x00000050 regs.Gprs[15] = testValue - + err = unix.PtraceSetRegs(pid, ®s) if err != nil { t.Fatalf("PtraceSetRegs (with >4GB value) failed: %v", err) } - + // Read back and verify var regs64 unix.PtraceRegs err = unix.PtraceGetRegs(pid, ®s64) if err != nil { t.Fatalf("PtraceGetRegs (verify >4GB) failed: %v", err) } - + if regs64.Gprs[15] != testValue { - t.Errorf("GPR[15] >4GB value not preserved: wrote 0x%016x, read 0x%016x", + t.Errorf("GPR[15] >4GB value not preserved: wrote 0x%016x, read 0x%016x", testValue, regs64.Gprs[15]) - t.Errorf(" High 32 bits: wrote 0x%08x, read 0x%08x", + t.Errorf(" High 32 bits: wrote 0x%08x, read 0x%08x", uint32(testValue>>32), uint32(regs64.Gprs[15]>>32)) } else { - t.Logf("GPR[15] >4GB value preserved: 0x%016x (high: 0x%08x)", + t.Logf("GPR[15] >4GB value preserved: 0x%016x (high: 0x%08x)", regs64.Gprs[15], uint32(regs64.Gprs[15]>>32)) } - + // Restore original value regs.Gprs[15] = origGPR15 err = unix.PtraceSetRegs(pid, ®s) if err != nil { t.Logf("Warning: Failed to restore GPR[15]: %v", err) } - + t.Logf("PtraceSetRegs successful - all registers preserved, >4GB values work") }) @@ -3674,10 +3674,10 @@ int main() { // A7F4 0010 = BRC 15,+16 (unconditional branch forward 16 bytes) insn := []byte{0xA7, 0xF4, 0x00, 0x10, 0x00, 0x00} pc := uint64(0x10000) - + dest := unix.CalculateBranchDest(pid, pc, insn) expected := uint64(pc + (0x10 * 2)) // Displacement is in halfwords - + if dest != expected && dest != unix.NO_BRANCH_DEST { t.Logf("CalculateBranchDest: got 0x%x, expected 0x%x (may vary based on register values)", dest, expected) } @@ -3708,17 +3708,17 @@ int main() { return 0, fmt.Errorf("failed to start new process: %v", err) } newPid := newCmd.Process.Pid - + // Give it time to start time.Sleep(50 * time.Millisecond) - + // Attach to new process err = unix.PtraceAttach(newPid) if err != nil { newCmd.Process.Kill() return 0, fmt.Errorf("failed to attach to new process: %v", err) } - + // Wait for attach to complete var status unix.WaitStatus _, err = unix.Wait4(newPid, &status, 0, nil) @@ -3727,13 +3727,13 @@ int main() { newCmd.Process.Kill() return 0, fmt.Errorf("wait after attach failed: %v", err) } - + return newPid, nil } // Declare status before any goto statements var status unix.WaitStatus - + // Continue execution briefly to get out of LE/system library code err := unix.PtraceCont(pid, 0) if err != nil { @@ -3748,7 +3748,7 @@ int main() { pid = newPid attached = true t.Logf("Respawned process with PID %d (already stopped and attached)", pid) - + // Respawned process is already stopped after attach, skip to single-step goto skipContinue } else { @@ -3818,7 +3818,7 @@ int main() { if err != nil { t.Fatalf("PtraceGetRegs failed: %v", err) } - + // Check PSW format and AMODE const ECMODE31BIT = 0x0000000000080000 const AMODE31BIT_ADDR = 0x0000000080000000 @@ -3875,7 +3875,7 @@ int main() { // Verify PC moved to one of the expected addresses if pc2 != nextPC && pc2 != branchDest { - t.Errorf("PC after step (0x%x) is neither nextPC (0x%x) nor branchDest (0x%x)", + t.Errorf("PC after step (0x%x) is neither nextPC (0x%x) nor branchDest (0x%x)", pc2, nextPC, branchDest) } else { t.Logf("Single step successful: PC moved from 0x%x to 0x%x", pc1, pc2) @@ -3900,12 +3900,12 @@ int main() { // z/Architecture format: check EA+BA bits isAmode64 = (regs.Psw.Mask & AMODE64BIT_MASK) == AMODE64BIT_MASK } - + prevPC := pc2 if !isAmode64 { prevPC = prevPC & 0x7FFFFFFF } - + for i := 0; i < 5; i++ { err := unix.PtraceSingleStep(pid) if err != nil { @@ -3936,7 +3936,7 @@ int main() { t.Skip("Process not attached") return } - + // Detach directly without continuing (process is already stopped) err := unix.PtraceDetach(pid) if err != nil && err != unix.EINTR && err != unix.ESRCH { @@ -3949,7 +3949,7 @@ int main() { t.Logf("Process already exited, detach not needed") } attached = false - + t.Logf("Successfully detached from process (or process exited)") }) @@ -3974,7 +3974,7 @@ int main() { t.Run(tc.name, func(t *testing.T) { length := unix.GetInstructionLength(tc.firstByte) if length != tc.expected { - t.Errorf("GetInstructionLength(0x%02x) = %d, expected %d", + t.Errorf("GetInstructionLength(0x%02x) = %d, expected %d", tc.firstByte, length, tc.expected) } }) @@ -4059,7 +4059,7 @@ int main() { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { dest := unix.CalculateBranchDest(pid, tc.pc, tc.insn) - + if tc.wantDest { if dest == unix.NO_BRANCH_DEST { t.Errorf("Expected branch destination, got NO_BRANCH_DEST") diff --git a/unix/ztypes_zos_s390x.go b/unix/ztypes_zos_s390x.go index 1d9d465fe..80d4c9535 100644 --- a/unix/ztypes_zos_s390x.go +++ b/unix/ztypes_zos_s390x.go @@ -209,16 +209,15 @@ type TCPInfo struct { Total_retrans uint32 } - // Ptrace structures for z/OS type PtraceRegs struct { - Psw PtracePsw - Gprs [16]uint64 - Acrs [16]uint32 - Orig_gpr2 uint64 - Fp_regs PtraceFpregs - Per_info PtracePer + Psw PtracePsw + Gprs [16]uint64 + Acrs [16]uint32 + Orig_gpr2 uint64 + Fp_regs PtraceFpregs + Per_info PtracePer Ieee_instruction_pointer uint64 } @@ -245,14 +244,15 @@ type PtracePer struct { // PT_BLOCKREQ structures for block ptrace requests // Layout matches BPXYPTRC HLASM macro (PtBR_GPR + PtBR_GPR_EXT): -// Offset 0-1: Writebitflags (PtBR_GPR_CntlGPR, 2 bytes) -// Offset 2-3: Wpsw (PtBR_GPR_CntlMisc, 2 bytes with WPSW bit flag) -// Offset 4-15: Reserved (12 bytes) -// Offset 16-79: Gpr (64 bytes, 16 GPRs) -// Offset 80-143: Ctl (64 bytes, 16 CRs) -// Offset 144-151: Psw old format (8 bytes, PtBR_GPR_PSW) -// Offset 152-167: Pswg extended (16 bytes, PtBR_GPR_PSWG for 64-bit) -// Total: 168 bytes (base 152 + extended 16) +// +// Offset 0-1: Writebitflags (PtBR_GPR_CntlGPR, 2 bytes) +// Offset 2-3: Wpsw (PtBR_GPR_CntlMisc, 2 bytes with WPSW bit flag) +// Offset 4-15: Reserved (12 bytes) +// Offset 16-79: Gpr (64 bytes, 16 GPRs) +// Offset 80-143: Ctl (64 bytes, 16 CRs) +// Offset 144-151: Psw old format (8 bytes, PtBR_GPR_PSW) +// Offset 152-167: Pswg extended (16 bytes, PtBR_GPR_PSWG for 64-bit) +// Total: 168 bytes (base 152 + extended 16) type PtraceBlkGpr struct { Writebitflags uint16 // 2 bytes at offset 0 (PtBR_GPR_CntlGPR) Wpsw uint16 // 2 bytes at offset 2 (PtBR_GPR_CntlMisc with WPSW flag) @@ -287,7 +287,6 @@ type PtraceBlkUar struct { _ [4]byte } - type _Gid_t uint32 type rusage_zos struct { From 3f3ff2477e05f4f5572abe9e14903f09cedb7ac9 Mon Sep 17 00:00:00 2001 From: Joon Lee Date: Tue, 25 Aug 2026 13:25:41 -0400 Subject: [PATCH 6/6] unix: fix GC-unsafe unsafe.Pointer(uintptr) in z/OS ptrace wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ptrace(), ptracePtr(), and ptracePtrWithBuffer() all passed integer values (signal numbers, register indices, byte counts, target-process addresses) to Bpx4ptr as unsafe.Pointer(integer_value). Converting an arbitrary uintptr to unsafe.Pointer is not valid Go: the garbage collector may treat the integer as a heap pointer, attempt to follow it, and crash or corrupt GC state. The fix is the same in all three functions: inline the parms array that Bpx4ptr would construct internally and place &local at the relevant slot. BPX4PTR dereferences parms[N] to read the value, so it receives the integer unchanged, while the GC only ever sees valid stack pointers. ptrace: &data at parms[3] (data uintptr — signal, etc.) ptracePtr: &addr at parms[2] (addr uintptr — target addr) ptracePtrWithBuffer: &addr at parms[2] (same) Addresses reviewer comments df9098da_8b5b0754 (PS2, line 59) and 588f8ca2_e78c1e4e (PS3, line 92) on CL 796660. --- unix/ptrace_zos.go | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/unix/ptrace_zos.go b/unix/ptrace_zos.go index 0c14c2fc9..394fbd71b 100644 --- a/unix/ptrace_zos.go +++ b/unix/ptrace_zos.go @@ -87,9 +87,25 @@ func ptrace(request int, pid int, addr uintptr, data uintptr) (err error) { } // ptracePtr is a variant that accepts unsafe.Pointer for the data parameter. +// addr is a target-process address (a raw integer), not a Go-managed pointer. +// We must not pass it to Bpx4ptr as unsafe.Pointer(addr); instead we inline the +// parms array and place &addr at parms[2], exactly as ptrace() does for data. func ptracePtr(request int, pid int, addr uintptr, data unsafe.Pointer) (err error) { - rv, rc, rn := Bpx4ptr(int32(request), int32(pid), - unsafe.Pointer(addr), data, nil) + req32 := int32(request) + pid32 := int32(pid) + var rv, rc, rn int32 + var buffer unsafe.Pointer + parms := [8]unsafe.Pointer{ + unsafe.Pointer(&req32), + unsafe.Pointer(&pid32), + unsafe.Pointer(&addr), // &addr, not unsafe.Pointer(addr) + unsafe.Pointer(&data), + unsafe.Pointer(&buffer), + unsafe.Pointer(&rv), + unsafe.Pointer(&rc), + unsafe.Pointer(&rn), + } + bpxcall(parms[:], BPX4PTR) if rv != 0 { err = errnoErr2(Errno(rc), uintptr(rn)) } @@ -97,9 +113,22 @@ func ptracePtr(request int, pid int, addr uintptr, data unsafe.Pointer) (err err } // ptracePtrWithBuffer is used for operations that require the buffer parameter. +// addr is a target-process address; same &addr treatment as ptracePtr. func ptracePtrWithBuffer(request int, pid int, addr uintptr, data unsafe.Pointer, buffer unsafe.Pointer) (err error) { - rv, rc, rn := Bpx4ptr(int32(request), int32(pid), - unsafe.Pointer(addr), data, buffer) + req32 := int32(request) + pid32 := int32(pid) + var rv, rc, rn int32 + parms := [8]unsafe.Pointer{ + unsafe.Pointer(&req32), + unsafe.Pointer(&pid32), + unsafe.Pointer(&addr), // &addr, not unsafe.Pointer(addr) + unsafe.Pointer(&data), + unsafe.Pointer(&buffer), + unsafe.Pointer(&rv), + unsafe.Pointer(&rc), + unsafe.Pointer(&rn), + } + bpxcall(parms[:], BPX4PTR) if rv != 0 { err = errnoErr2(Errno(rc), uintptr(rn)) }