// 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. //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. // // 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) { 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. // 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) { 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)) } return } // 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) { 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)) } 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) }