procmem

package
v0.7.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package procmem provides ptrace-based process memory access primitives.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ReadMem

func ReadMem(pid int, addr uintptr, buf []byte) (int, error)

ReadMem reads len(buf) bytes from addr in pid using process_vm_readv. Does not require an active ptrace stop; needs CAP_SYS_PTRACE or an existing ptrace relationship with the target.

func WriteMem

func WriteMem(pid int, addr uintptr, buf []byte) (int, error)

WriteMem writes buf into addr in pid using process_vm_writev. Fails on write-protected pages (e.g. r-xp mappings); use PokeText for those.

Types

type Tracer

type Tracer struct {
	// contains filtered or unexported fields
}

Tracer wraps ptrace for a single tracee. All ptrace calls are dispatched to a dedicated goroutine that calls runtime.LockOSThread at startup and never releases it, satisfying the Linux kernel requirement that every ptrace call for a given tracee come from the same OS thread that issued PTRACE_ATTACH.

This requirement is exact-thread, not just same-thread-group: PTRACE_TRACEME records the tracee's tracer as the specific task that was its parent at the moment TRACEME ran, i.e. whichever OS thread performed the fork. A wait4 for that tracee's initial stop succeeds from any thread in the process (TRACEME leaves the tracee's real parent and ptrace-parent identical, so ordinary thread-group-wide wait eligibility applies) — but a subsequent ptrace request such as PTRACE_GETREGS does not: it is checked against that exact task, and fails with ESRCH from any other thread, even one in the same process. So the fork itself must happen on this Tracer's pinned thread too; see StartAndFollowChild.

func NewTracer

func NewTracer() *Tracer

NewTracer creates a Tracer with its pinned OS thread already running.

func (*Tracer) Attach

func (t *Tracer) Attach(pid int) error

Attach calls PTRACE_ATTACH and waits for the tracee to stop.

func (*Tracer) Cont

func (t *Tracer) Cont(sig int) error

Cont resumes the tracee. sig is forwarded as a signal (0 for none).

func (*Tracer) ContPID added in v0.2.0

func (t *Tracer) ContPID(pid, sig int) error

ContPID resumes an arbitrary ptrace-stopped PID on the pinned OS thread. sig is forwarded to the resumed process (0 for no signal).

func (*Tracer) Detach

func (t *Tracer) Detach() error

Detach calls PTRACE_DETACH, allowing the tracee to resume.

func (*Tracer) DetachAll added in v0.2.0

func (t *Tracer) DetachAll(pids []int) error

DetachAll interrupts and detaches from each PID in the list on the pinned OS thread. Errors for processes that no longer exist are silently ignored. Uses the same PTRACE_INTERRUPT → SIGSTOP fallback as InterruptDetach.

func (*Tracer) FollowChild

func (t *Tracer) FollowChild(pid int) error

FollowChild sets up the Tracer for a child that was started with SysProcAttr{Ptrace: true}. The child calls PTRACE_TRACEME before exec and then stops on SIGTRAP; this method collects that stop without issuing PTRACE_ATTACH. Use this in tests and any context where you own the child process. Use Attach for attaching to an already-running process.

The caller must have started pid from this same Tracer's pinned thread — via StartAndFollowChild — not via a plain cmd.Start() on some other goroutine. See the Tracer doc comment for why: a wait4 for the initial stop works from any thread, but this Tracer's later ptrace requests (GetRegs, PokeText, ...) require the fork to have happened on its own pinned thread.

func (*Tracer) GetEventMsgPID added in v0.2.0

func (t *Tracer) GetEventMsgPID(pid int) (uint, error)

GetEventMsgPID retrieves the ptrace event message from an arbitrary ptrace-stopped PID. After PTRACE_EVENT_FORK or PTRACE_EVENT_VFORK, this returns the newly created child's PID.

func (*Tracer) GetRegs

func (t *Tracer) GetRegs() (*unix.PtraceRegs, error)

GetRegs reads the tracee's general-purpose registers.

func (*Tracer) InterruptDetach added in v0.2.0

func (t *Tracer) InterruptDetach() error

InterruptDetach stops the current tracee and detaches. For PTRACE_SEIZE-based tracees PTRACE_INTERRUPT is used; for PTRACE_ATTACH or PTRACE_TRACEME tracees (where PTRACE_INTERRUPT returns EIO) we fall back to SIGSTOP.

func (*Tracer) PokeText

func (t *Tracer) PokeText(addr uintptr, buf []byte) error

PokeText writes buf into the tracee's address space using PTRACE_POKETEXT. Unlike WriteMem, this is permitted on read-only-but-executable pages (e.g. the vDSO), which is how debuggers set breakpoints and how we patch clock_gettime. Requires an active ptrace attachment.

func (*Tracer) Seize added in v0.3.0

func (t *Tracer) Seize(pid int) error

Seize attaches to pid via PTRACE_SEIZE and immediately stops it with PTRACE_INTERRUPT. Unlike Attach (PTRACE_ATTACH), SEIZE does not deliver SIGSTOP to the tracee's thread group, avoiding group-stop races during subsequent ptrace operations. PTRACE_INTERRUPT works reliably on SEIZE-attached processes, which makes InterruptDetach safe to call even while the tracee is running.

func (*Tracer) SetOptions added in v0.2.0

func (t *Tracer) SetOptions(opts int) error

SetOptions sets PTRACE_O_* options on the current tracee. Must be called while the tracee is ptrace-stopped.

func (*Tracer) SetOptionsPID added in v0.2.0

func (t *Tracer) SetOptionsPID(pid, opts int) error

SetOptionsPID sets PTRACE_O_* options on an arbitrary ptrace-stopped PID without changing the Tracer's primary tracee (t.pid).

func (*Tracer) SetRegs

func (t *Tracer) SetRegs(r *unix.PtraceRegs) error

SetRegs writes the tracee's general-purpose registers.

func (*Tracer) SetTracee added in v0.2.0

func (t *Tracer) SetTracee(pid int)

SetTracee changes the PID that Tracer methods (GetRegs, SetRegs, PokeText, Cont, Wait) operate on. Used when temporarily injecting into a process other than the primary tracee (e.g. a child that just exec'd). The caller is responsible for restoring the original PID when done.

func (*Tracer) SingleStep

func (t *Tracer) SingleStep() error

SingleStep resumes the tracee for exactly one instruction, then stops it again.

func (*Tracer) StartAndFollowChild added in v0.7.1

func (t *Tracer) StartAndFollowChild(cmd *exec.Cmd) (int, error)

StartAndFollowChild starts cmd (which must already have SysProcAttr.Ptrace set) and waits for its initial post-execve ptrace stop, all on this Tracer's own pinned OS thread. The caller must not call cmd.Start() itself.

This is the correct way to start a traced child: PTRACE_TRACEME ties the tracee's tracer to the exact thread that performed the fork, and every subsequent ptrace request this Tracer issues (GetRegs, PokeText, Cont, ...) must come from that same thread. Calling cmd.Start() separately, on whatever thread the caller's goroutine happens to be scheduled on, and only later creating a Tracer to follow the result, binds the fork to a different thread than the one that will drive those later requests — a mismatch that intermittently (not always) makes them fail with ESRCH, since it depends on whether the Go scheduler happens to reuse the same underlying OS thread.

func (*Tracer) Wait

func (t *Tracer) Wait() (unix.WaitStatus, error)

Wait blocks until the tracee stops or exits and returns its wait status.

func (*Tracer) WaitAnyNonBlocking added in v0.2.0

func (t *Tracer) WaitAnyNonBlocking() (int, unix.WaitStatus, error)

WaitAnyNonBlocking checks for a stop event from any traced child without blocking. Returns pid=0 if no events are pending, or syscall.ECHILD if there are no traced children.

Uses WNOTHREAD to restrict reaping to tracees of this Tracer's own pinned OS thread. Without it, wait4(-1, ...) reaps children across every thread in the calling thread group by default (Linux only skips other threads when __WNOTHREAD is set) — so when a process hosts multiple independent Tracers concurrently (e.g. one ChildTracker per process added to a faketime Session), one Tracer's wait4 can steal a ptrace-stop that actually belongs to a different Tracer's tracee. The stealing Tracer doesn't recognize the pid, its PTRACE_CONT fallback fails silently with ESRCH (ptrace ops are still correctly restricted to the registered tracer thread), and the rightful owner never observes the event again — leaving that tracee permanently ptrace-stopped. This was observed as sessions with many concurrently tracked process trees (e.g. Postgres, Redpanda, and a mix of Go/Python services all under one Session) silently hanging.

Also passes WUNTRACED, WCONTINUED, and WALL/__WALL: heavily multi-threaded tracees (e.g. Redpanda's Seastar reactor, one thread per shard) rely on wait4 reporting stop/continue transitions for clone-created threads, not just SIGCHLD-signaling children — the default filtering otherwise applied to non-ptrace waits.

func (*Tracer) WaitPIDNonBlocking added in v0.7.1

func (t *Tracer) WaitPIDNonBlocking(pid int) (unix.WaitStatus, bool, error)

WaitPIDNonBlocking checks for a stop or exit event on one specific, already-known pid without blocking. ok is false if no event is pending for pid right now.

Unlike WaitAnyNonBlocking, this can never observe an event belonging to a different pid, so it carries none of that method's cross-Tracer hazard: it is safe to call concurrently from many independent Tracers (e.g. one ChildTracker per process in a faketime Session using WithTracking, all forked by the same caller and so all real children of the same thread) even though WNOTHREAD only restricts wait4(-1, ...) to children of the calling thread specifically, not children of the calling thread's whole process — an exact-pid wait has no such ambiguity to exploit, because the kernel can only ever report status for the pid actually asked for.

Also passes WUNTRACED, WCONTINUED, and WALL/__WALL — see WaitAnyNonBlocking for why: pid may be a clone-created thread of a heavily multi-threaded tracee, not just a SIGCHLD-signaling child.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL