gspy
Forensic goroutine-to-syscall inspector for live Go processes
About gspy
gspy — eBPF Forensic Inspector for Go Binaries & Malware Analysis
Developed by Mutasem Kharma (معتصم خرما)
gspy attaches to a running Linux Go process and correlates syscall events with goroutine IDs using eBPF. Use it when you need to investigate an existing binary without recompiling it or adding a diagnostics agent.
The tracer uses kernel uprobes and tracepoints. Observation has overhead; this project does not claim zero impact or complete forensic preservation. --readonly records the executable file's SHA-256 and marks JSON events; it is not a hash of process memory or a guarantee that instrumentation leaves memory unchanged.
⚡ Core Features
- Attach by PID and inspect goroutine-to-syscall mappings in a terminal UI.
- Filter I/O, network, or scheduling syscalls in both the UI and JSONL output.
- Resolve runtime probe offsets from Go's PC table, including stripped binaries.
- Build for Linux AMD64 and ARM64; see CI for tested compiler versions.
🚀 Demo
(Generated using the reproducible script in demo/)
🧠 Technical Architecture
Traditional tracing tools (strace) intercept syscalls via thread IDs (TIDs), blinding responders to what internal Go concurrent operation triggered it.
gspy solves this by weaponizing eBPF to bridge the OS and the Go runtime:
graph TD
classDef bpf fill:#f96,stroke:#333,stroke-width:2px;
classDef user fill:#6495ED,stroke:#333,stroke-width:2px,color:#fff;
classDef target fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff;
A[gspy Userspace]:::user -->|Inject compiled map| BPF(eBPF VM):::bpf
Target[Target Go Binary]:::target -->|Triggers| BPF
BPF -->|uprobe on runtime.execute| C{Extract Goroutine ID}
C -->|TID to GID Map| BPF_Map[(BPF Hash Map)]:::bpf
BPF -->|raw_syscalls/sys_enter| D{Syscall Intercept}
D -->|Lookup GID via TID| BPF_Map
D -->|Emit Payload| RingBuf[(16MB BPF RingBuf)]:::bpf
RingBuf -->|Poll| A
A -->|Read ELF symbols and PC table| Symbols[Executable File]
Symbols -->|Resolve Captured PC| TUI[Terminal UI]:::user
- Uprobes: Hook
runtime.executeto track Go scheduler context switches, extracting the goroutine ID (goid) from theruntime.gstruct. - Tracepoints: Intercept
sys_enter/sys_exit, joining the OS Thread ID (TID) against the active goroutine map. - Userspace Symbolization: Reads the executable's ELF symbols and Go PC table to resolve captured instruction pointers. Process-memory reads recover probe context; they do not read the ELF symbol table.
For a deeper dive into the engineering, read: Why Ptrace is Dead for Go Forensics
🚀 Quick Start (Demo)
Install the source-build prerequisites listed below before running the demo:
# Clone and run the automated demo
git clone https://github.com/Mutasem-mk4/gspy
cd gspy
./demo/demo.sh
This builds gspy, starts a synthetic target that attempts localhost connections and writes to a private temporary log, and opens the terminal UI. Press q to detach; the script stops its demo target and removes its temporary log. No internet connection is used by the target.
📋 Compatibility Matrix
gspy rigidly tracks the internal Application Binary Interface (ABI) of the Go compiler.
ABI tests compile real ELF targets with Go 1.23.0, 1.24.0, 1.25.0, 1.26.8, and 1.27.1 for AMD64 and ARM64. ABI offset checks alone do not verify live attachment. The manual portfolio QA additionally exercises live stripped targets built with Go 1.23.0, 1.26.8, and 1.27.1 on native Linux runners.
If DWARF does not provide runtime.g.goid and the runtime is absent from the verified offset table, gspy exits before attaching BPF probes. The diagnostic identifies the unknown version and asks for DWARF or a supported runtime; it never guesses an offset. DWARF-derived offsets are not a guarantee that every other runtime detail is compatible.
If DWARF does not provide runtime.g.goid and the runtime is absent from the verified offset table, gspy exits before attaching BPF probes. The diagnostic identifies the unknown version and asks for DWARF or a supported runtime; it never guesses an offset. DWARF-derived offsets are not a guarantee that every other runtime detail is compatible.
Compiler patch releases and different kernels can behave differently. Consult the latest CI results before relying on a particular combination.
Linux Kernel Constraints
- Linux kernel >= 5.8 (Mandatory for BPF ring buffer support)
CONFIG_BPF_SYSCALL=yCONFIG_DEBUG_INFO_BTF=y(Recommended for CO-RE portability)
🛠️ Installation
Official Distros
gspy is an official package in the following security-focused distributions:
- BlackArch Linux:
pacman -S gspy - Kali Linux: (Pending)
- Parrot OS: (Pending)
Compile from Source
git clone https://github.com/Mutasem-mk4/gspy
cd gspy
make generate # requires clang >= 14 and LLVM
make build # requires Go >= 1.26.8
sudo make install # installs the binary and existing man page
Privileges
Grant Linux capabilities to run securely without enforcing sudo:
sudo setcap cap_bpf,cap_perfmon+ep /usr/bin/gspy
💻 Usage
gspy <pid> # Show live goroutine→syscall mapping TUI
gspy <pid> --top # Sort by total syscall volume (default)
gspy <pid> --latency # Sort strictly by highest syscall response blockage
gspy <pid> --filter <mode> # Subselect modes: io | net | sched | all
gspy <pid> --readonly # Record executable file SHA-256 and mark JSON events
gspy <pid> --json # Export data as newline-delimited JSON stream for SIEM / jq pipelines
gspy <pid> --debug # Trace BPF verifier logs and map statistics
gspy --version # Print release info
The TUI io, net, and sched filters show each goroutine's last matching
syscall received in the preceding five seconds. Later unrelated syscalls do not
hide that activity. The displayed syscall, latency, and frame belong to that
matching event; COUNT remains the total syscall count since attachment. all
shows the latest event without this time window. JSONL filtering still applies
to individual events, not to this TUI window.
Generic descriptor operations (epoll_*, poll, select, and sendfile) are
classified as I/O, since they do not establish that the descriptors are sockets.
The network category contains socket-specific syscalls. Neither category proves
that a connection succeeded.
G counts observed goroutines with known IDs, not every goroutine in the target.
Unattributed events appear under ? and retain gid=0 in saved JSON. The detail
view shows a captured frame, not a complete stack trace. Function names are
resolved from ELF symbols or Go's retained PC table for stripped binaries;
unresolved addresses remain hexadecimal.
🤝 Contributing
JSONL emits one object per syscall event: count is 1 for each record, latency_us is the observed syscall duration in microseconds, and ts is the kernel monotonic timestamp in seconds, not Unix wall-clock time. gid=0 means the event could not be attributed to a known goroutine. These records are not the aggregated counters displayed in the TUI. A failed JSON output write terminates tracing with a nonzero exit status.
We actively welcome Pull Requests solving compatibility with newer Go betas or hardening the BPF C-code. Check out the Contributing Guide and Code of Conduct.
📄 License & Legal
GPL-2.0-only. See LICENSE for the full text.
eBPF kernel ecosystem interactions mandate GPL adherence. All source files explicitly carry SPDX-License-Identifier headers to ensure Debian licensecheck and enterprise compliance out-of-the-box.
Developed by Mutasem Kharma (معتصم خرما) — GitHub | Portfolio | Twitter/X
Runtime layout and offline packaging
Go 1.25, 1.26, and 1.27 runtime layouts are checked against real compiled ELF files for amd64 and arm64, with and without DWARF information. Unknown stripped runtime versions are refused; rebuild the target with DWARF information to derive the offset. CLI options work before or after the PID.
Build with Go 1.26.8 or newer. bpf2go is a pinned Go tool in go.mod, so no
separate global installation is needed. make build generates the BPF bindings.
For offline Debian builds, run make source-dist before building from the source
archive. The archive includes dependencies and their upstream license files;
Debian rules reject source trees without vendor/modules.txt and disable downloads.
Linux ARM64 resolves syscall names using the native generic syscall ABI. CI checks real tracing against an announced goroutine ID on native amd64 and arm64 runners.
Frequently Asked Questions
What is gspy?
gspy is a Security library for the Go programming language. Forensic goroutine-to-syscall inspector for live Go processes
How do I install gspy?
Install gspy with the Go module system using `go get Mutasem-mk4/gspy`. Check the repository for the current installation instructions.
What category does gspy belong to?
gspy is listed under Security, specifically Security.