park: Pause a process and free its TCP port via ptrace/Mach injection

Hi everyone,

I built a CLI tool in Go called park that solves a small but annoying problem: freeing a TCP port without killing the process that owns it.

Demo: Park Usage Demo - asciinema.org

The idea: you’re running a dev server on :8080. You need that port for something else. Instead of killing the server (and losing its warm state), you run park 8080 — the process freezes and the port genuinely
frees. When you’re done, park resume 8080 brings it back. Same PID, same memory.

Install:
curl -fsSL https://raw.githubusercontent.com/mr-vaibh/park/main/install.sh | sh

GitHub: GitHub - mr-vaibh/park: Pause a process and free its TCP port. Resume later, same PID and memory intact. Ctrl+Z for servers. · GitHub
Website: park - Pause processes, free ports, resume later

How it works

SIGSTOP alone doesn’t free the port — the kernel keeps the socket bound. park actually reaches inside the target process and injects syscalls to close the listening fd.

Linux (x86_64): Pure Go, no CGO. Uses ptrace to attach, finds the listener fd via /proc/net/tcp, writes a syscall; int3 stub to the vdso page, injects close(fd). Resume injects socket + setsockopt + bind + listen + dup2 to recreate the listener at the original fd number.

macOS (Apple Silicon): Uses CGO for Mach APIs. task_for_pid to get the task port, mach_vm_allocate for scratch pages (16KB each — Apple Silicon page size), thread_create_running to spawn a fresh
injection thread inside the target. The stub is arm64 LDR + svc + brk instructions with PC-relative loads from a separate data page. Breakpoints are caught via a Mach exception port. Resume also injects
kevent() to re-register the listener with kqueue and fcntl(O_NONBLOCK) so async frameworks survive.

Some Go-specific details

  • Build tags: ptrace_linux_amd64.go (pure Go), ptrace_darwin_arm64.go (CGO + //go:build darwin && arm64 && cgo), ptrace_darwin.go (fallback stub for !cgo or !arm64). Cross-compilation from any host
    works because the CGO path is gated.

  • The arm64 instruction encoding (encodeLdrLiteral, encodeBR) is done in pure Go at runtime — no assembler, no hardcoded bytes. The stub bytes are computed from the actual scratch page addresses.

  • One external dependency: golang.org/x/sys for the Linux ptrace wrappers.

  • CI runs the full integration test on ubuntu-latest: spawns an HTTP server, parks it, binds something else to the port, resumes, and asserts it still serves.

Tested with python http.server, uvicorn/FastAPI, Flask. Works on macOS Sequoia (M1) and Linux x86_64.

Would love feedback from anyone who’s worked with ptrace or Mach APIs in Go. The Mach exception port handling in particular was an adventure — happy to discuss.

1 Like