diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..759cd9d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,26 @@
+name: release
+
+on:
+ push:
+ tags:
+ - v*
+
+jobs:
+ build:
+ name: releasing
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-go@v3
+ with:
+ go-version: "1.19"
+ - uses: goreleaser/goreleaser-action@v3
+ with:
+ version: latest
+ args: release --rm-dist
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..c3487fd
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,22 @@
+name: tests
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+jobs:
+ test:
+ name: tests
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ ubuntu-latest ]
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: '1.19'
+ cache: true
+ - name: Run tests
+ run: sudo apt install -y musl-tools && make test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ddd5654
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.idea
+/grace
+/dist
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..f6da274
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,33 @@
+before:
+ hooks:
+ - sudo apt install -y musl-tools
+
+builds:
+ - id: grace
+ main: .
+ binary: grace
+ ldflags:
+ - "-linkmode external -s -w -extldflags '-fno-PIC -static'"
+ env:
+ - CGO_ENABLED=1
+ - CC=musl-gcc
+ goos:
+ - linux
+ goarch:
+ - "amd64"
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - "^docs:"
+ - "^test:"
+
+archives:
+ - format: binary
+ name_template: "{{ .Binary}}-{{ .Os }}-{{ .Arch }}"
+
+release:
+ prerelease: auto
+ github:
+ owner: liamg
+ name: grace
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..a56c7a2
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,7 @@
+# Contributing
+
+## Useful Links
+
+- [List of syscalls with descriptions](https://linuxhint.com/list_of_linux_syscalls/)
+- [Syscall table](https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md)
+- [Another Syscall table](https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/)
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..68a49da
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ea043d5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+default: build
+
+.PHONY: test
+test:
+ CC=musl-gcc CGO_ENABLED=1 go test ./...
+
+.PHONY: build
+build:
+ CGO_ENABLED=1 CC=musl-gcc go build --ldflags '-linkmode external -extldflags "-static"'
+
+.PHONY: demo
+demo: build
+ ./grace -- cat /dev/null
diff --git a/README.md b/README.md
index 74643a8..14d23fa 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,51 @@
# grace
-It's strace, with colours.
+
+_grace_ is a tool for monitoring and modifying syscalls for a given process.
+
+It's essentially a lightweight [strace](https://en.wikipedia.org/wiki/Strace), in Go, with colours and pretty output.
+
+![](screenshot.png)
+
+// TODO: new screenshot
+
+## Features/Usage Examples
+
+### grace vs. strace
+
+_grace_ isn't meant to compete with _strace_, it's purely meant to be a user-friendly, lightweight alternative. However, the following should provide a rough idea of what is supported in _grace_ so far.
+
+| Feature | grace | strace |
+|---------------------------------------------------------------------------------------|-------|--------|
+| Start a program and print all syscalls it makes | ✅ | ✅ |
+| Attach to an existing process by `pid` and print all syscalls it makes | ✅ | ✅ |
+| Filter syscalls by name, e.g. only show occurrences of the `open` syscall | ❌ | ✅ |
+| Filter syscalls using a given path, e.g. only show syscalls that access `/etc/passwd` | ❌ | ✅ |
+| Dump I/O for certain file descriptors | ❌ | ✅ |
+| Count occurrences and duration of all syscalls and present in a useful format | ❌ | ✅ |
+| Print relative/absolute timestamps | ❌ | ✅ |
+| Tamper with syscalls | ❌ | ✅ |
+| Print extra information about file descriptors, such as path, socket addresses etc. | some | ✅ |
+| Print stack traces | ❌ | ✅ |
+| Filter by return value | ❌ | ✅ |
+| Decode SELinux context info | ❌ | ✅ |
+| Pretty colours to make output easier to read | ✅ | ❌ |
+
+### Usage Examples
+
+```
+// TODO
+```
+
+## Installation
+
+Grab a statically compiled binary from the [latest release](https://github.com/liamg/grace/releases/latest).
+
+## Build Dependencies
+
+If you want to build _grace_ yourself instead of using the precompiled binaries, you'll need a recent version of Go (1.19+), `musl-gcc` installed (you can install `musl-tools` on Ubuntu or `musl` on Arch), and kernel headers (install `linux-headers-$(uname -r)` on Ubuntu or `linux-headers` on Arch). _grace_ mainly just pulls constants from the kernel headers, so it's not a huge dependency. You should then have some success running `make build`. Note that many architectures are not yet supported (see below.)
+
+## Supported Platforms/Architecture
+
+Currently only Linux/amd64 is supported. Other architectures coming soon.
+
+If you'd like to implement a new architecture, you can duplicate `tracer/sys_amd64.go` and convert it to contain the syscall definitions for your arch.
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..3a81088
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,17 @@
+module github.com/liamg/grace
+
+go 1.19
+
+require (
+ github.com/spf13/cobra v1.6.1
+ github.com/stretchr/testify v1.8.1
+ golang.org/x/sys v0.1.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/inconshreveable/mousetrap v1.0.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..fb9e32a
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,27 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..bf690bd
--- /dev/null
+++ b/main.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/liamg/grace/printer"
+
+ "github.com/liamg/grace/tracer"
+ "github.com/spf13/cobra"
+)
+
+var (
+ flagDisableColours = false
+ flagMaxStringLen = 32
+ flagHexDumpLongStrings = true
+ flagMaxHexDumpLen = 4096
+ flagPID = 0
+ flagSuppressOutput = false
+ flagMaxObjectProperties = 2
+ flagVerbose = false
+ flagExtraNewLine = false
+ flagMultiline = false
+)
+
+var rootCmd = &cobra.Command{
+ Use: "grace [flags] [command [args]]",
+ Example: `grace -- cat /etc/passwd`,
+ Short: `grace is a CLI tool for monitoring and modifying syscalls for a given process.
+
+It's essentially strace, in Go, with colours and pretty output.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cmd.SilenceErrors = true
+ cmd.SilenceUsage = true
+
+ if len(args) == 0 && flagPID == 0 {
+ return cmd.Help()
+ }
+
+ var t *tracer.Tracer
+ var err error
+ if flagPID > 0 {
+ t = tracer.New(flagPID)
+ } else {
+ t, err = tracer.FromCommand(flagSuppressOutput, args[0], args[1:]...)
+ if err != nil {
+ return err
+ }
+ }
+
+ p := printer.New(cmd.OutOrStdout())
+
+ p.SetUseColours(!flagDisableColours)
+ p.SetMaxStringLen(flagMaxStringLen)
+ p.SetMaxHexDumpLen(flagMaxHexDumpLen)
+ p.SetExtraNewLine(flagExtraNewLine)
+ p.SetMultiLine(flagMultiline)
+
+ if flagVerbose {
+ p.SetHexDumpLongStrings(true)
+ p.SetMaxObjectProperties(0)
+ } else {
+ p.SetHexDumpLongStrings(flagHexDumpLongStrings)
+ p.SetMaxObjectProperties(flagMaxObjectProperties)
+ }
+
+ t.SetSyscallEnterHandler(p.PrintSyscallEnter)
+ t.SetSyscallExitHandler(p.PrintSyscallExit)
+ t.SetSignalHandler(p.PrintSignal)
+ t.SetProcessExitHandler(p.PrintProcessExit)
+
+ // TODO: set signal handler!
+
+ defer func() { _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "") }()
+
+ return t.Start()
+ },
+}
+
+func init() {
+ rootCmd.Flags().BoolVarP(&flagDisableColours, "no-colours", "C", flagDisableColours, "disable colours in output")
+ rootCmd.Flags().IntVarP(&flagMaxStringLen, "max-string-len", "s", flagMaxStringLen, "maximum length of strings to print")
+ rootCmd.Flags().BoolVarP(&flagHexDumpLongStrings, "hex-dump-long-strings", "x", flagHexDumpLongStrings, "hex dump strings longer than --max-string-len")
+ rootCmd.Flags().IntVarP(&flagMaxHexDumpLen, "max-hex-dump-len", "l", flagMaxHexDumpLen, "maximum length of hex dumps")
+ rootCmd.Flags().IntVarP(&flagPID, "pid", "p", flagPID, "trace an existing process by PID")
+ rootCmd.Flags().BoolVarP(&flagSuppressOutput, "suppress-output", "S", flagSuppressOutput, "suppress output of command")
+ rootCmd.Flags().IntVarP(&flagMaxObjectProperties, "max-object-properties", "o", flagMaxObjectProperties, "maximum number of properties to print for objects (recursive) - this also applies to array elements")
+ rootCmd.Flags().BoolVarP(&flagVerbose, "verbose", "v", flagVerbose, "enable verbose output (overrides other verbosity settings)")
+ rootCmd.Flags().BoolVarP(&flagExtraNewLine, "extra-newline", "n", flagExtraNewLine, "print an extra newline after each syscall to aid readability")
+ rootCmd.Flags().BoolVarP(&flagMultiline, "multiline", "m", flagMultiline, "print each syscall argument on a separate line to aid readability")
+}
+
+func main() {
+
+ if err := rootCmd.Execute(); err != nil {
+ if err.Error() == "no such process" {
+ os.Exit(0)
+ }
+ _, _ = fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
diff --git a/pipe b/pipe
new file mode 100755
index 0000000..ec4e37f
Binary files /dev/null and b/pipe differ
diff --git a/printer/arg.go b/printer/arg.go
new file mode 100644
index 0000000..8c30c62
--- /dev/null
+++ b/printer/arg.go
@@ -0,0 +1,70 @@
+package printer
+
+import (
+ "strings"
+
+ "github.com/liamg/grace/tracer"
+)
+
+func (p *Printer) PrintArg(arg tracer.Arg, exit bool) {
+
+ var indent int
+ if p.multiline {
+ indent = indentSize
+ }
+
+ if p.multiline {
+ p.Print(strings.Repeat(" ", indent))
+ }
+
+ if name := arg.Name(); name != "" {
+ p.PrintDim("%s: ", name)
+ }
+
+ p.PrintArgValue(&arg, p.nextColour(), exit, 0, indent)
+}
+
+func (p *Printer) NewLine(indent int) {
+ p.Print("\n" + strings.Repeat(" ", indent))
+}
+
+func (p *Printer) PrintArgValue(arg *tracer.Arg, colour Colour, exit bool, propCount int, indent int) int {
+
+ if arg.ReplaceValueWithAnnotation() {
+ p.PrintColour(colour, "%s", arg.Annotation())
+ return propCount
+ }
+
+ switch arg.Type() {
+ case tracer.ArgTypeData:
+ data := arg.Data()
+ if p.maxStringLen > 0 && len(data) > p.maxStringLen {
+ if exit && p.hexDumpLongStrings {
+ p.HexDump(arg.Raw(), arg.Data(), indent)
+ return propCount
+ }
+ data = append(data[:p.maxStringLen], []byte("...")...)
+ }
+ p.PrintColour(colour, "%q", string(data))
+ //p.PrintDim(" @ 0x%x", arg.Raw())
+ case tracer.ArgTypeInt, tracer.ArgTypeLong, tracer.ArgTypeUnsignedInt, tracer.ArgTypeUnsignedLong, tracer.ArgTypeUnknown:
+ p.PrintColour(colour, "%d", arg.Int())
+ case tracer.ArgTypeErrorCode:
+ p.printError(colour, arg)
+ case tracer.ArgTypeAddress:
+ p.PrintColour(colour, "0x%x", arg.Raw())
+ case tracer.ArgTypeObject, tracer.ArgTypeStat, tracer.ArgTypeSigAction:
+ propCount += p.printObject(arg.Object(), colour, exit, propCount, indent)
+ case tracer.ArgTypeIntArray, tracer.ArgTypePollFdArray, tracer.ArgTypeIovecArray:
+ propCount += p.printArray(arg.Array(), colour, exit, propCount, indent)
+ default:
+ // TODO: error here
+ p.PrintColour(colour, "%d (?)", arg.Raw())
+ }
+
+ if annotation := arg.Annotation(); annotation != "" {
+ p.PrintDim(" -> %s", annotation)
+ }
+
+ return propCount
+}
diff --git a/printer/array.go b/printer/array.go
new file mode 100644
index 0000000..aa32bc9
--- /dev/null
+++ b/printer/array.go
@@ -0,0 +1,32 @@
+package printer
+
+import "github.com/liamg/grace/tracer"
+
+func (p *Printer) printArray(arr []tracer.Arg, colour Colour, exit bool, count int, indent int) int {
+ prevIndent := indent
+ indent += indentSize
+ p.PrintColour(colour, "[")
+
+ for i, prop := range arr {
+ if p.multiline {
+ p.NewLine(indent)
+ }
+ colour := colours[i%len(colours)]
+ p.PrintDim("%d", i)
+ p.PrintDim(": ")
+ count += p.PrintArgValue(&prop, colour, exit, count, indent+indentSize)
+ if i < len(arr)-1 {
+ p.PrintDim(", ")
+ }
+ if p.maxObjectProperties > 0 && count >= p.maxObjectProperties && count < len(arr) {
+ p.PrintDim("...")
+ break
+ }
+ count++
+ }
+ if p.multiline && len(arr) > 0 {
+ p.NewLine(prevIndent)
+ }
+ p.PrintColour(colour, "]")
+ return count
+}
diff --git a/printer/colours.go b/printer/colours.go
new file mode 100644
index 0000000..2c1bd08
--- /dev/null
+++ b/printer/colours.go
@@ -0,0 +1,32 @@
+package printer
+
+type Colour int
+
+const (
+ ColourRed Colour = iota + 31
+ ColourGreen
+ ColourYellow
+ ColourBlue
+ ColourMagenta
+ ColourCyan
+ ColourWhite
+)
+
+var ColourDefault Colour = 0
+
+var colours = []Colour{
+ ColourBlue,
+ ColourYellow,
+ ColourGreen,
+ ColourRed,
+}
+
+func (p *Printer) currentColour() Colour {
+ return colours[p.colourIndex%len(colours)]
+}
+
+func (p *Printer) nextColour() Colour {
+ colour := colours[p.colourIndex%len(colours)]
+ p.colourIndex++
+ return colour
+}
diff --git a/printer/error.go b/printer/error.go
new file mode 100644
index 0000000..9cb3a2a
--- /dev/null
+++ b/printer/error.go
@@ -0,0 +1,286 @@
+package printer
+
+import (
+ "syscall"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/liamg/grace/tracer"
+)
+
+func (p *Printer) printError(colour Colour, arg *tracer.Arg) {
+
+ code := -arg.Int()
+ if code == 0 {
+ p.PrintColour(ColourGreen, "0")
+ return
+ }
+
+ constant := "unknown"
+
+ switch syscall.Errno(code) {
+ case unix.E2BIG:
+ constant = "E2BIG"
+ case unix.EACCES:
+ constant = "EACCES"
+ case unix.EADDRINUSE:
+ constant = "EADDRINUSE"
+ case unix.EADDRNOTAVAIL:
+ constant = "EADDRNOTAVAIL"
+ case unix.EADV:
+ constant = "EADV"
+ case unix.EAFNOSUPPORT:
+ constant = "EAFNOSUPPORT"
+ case unix.EAGAIN:
+ constant = "EAGAIN"
+ case unix.EALREADY:
+ constant = "EALREADY"
+ case unix.EBADE:
+ constant = "EBADE"
+ case unix.EBADF:
+ constant = "EBADF"
+ case unix.EBADFD:
+ constant = "EBADFD"
+ case unix.EBADMSG:
+ constant = "EBADMSG"
+ case unix.EBADR:
+ constant = "EBADR"
+ case unix.EBADRQC:
+ constant = "EBADRQC"
+ case unix.EBADSLT:
+ constant = "EBADSLT"
+ case unix.EBFONT:
+ constant = "EBFONT"
+ case unix.EBUSY:
+ constant = "EBUSY"
+ case unix.ECANCELED:
+ constant = "ECANCELED"
+ case unix.ECHILD:
+ constant = "ECHILD"
+ case unix.ECHRNG:
+ constant = "ECHRNG"
+ case unix.ECOMM:
+ constant = "ECOMM"
+ case unix.ECONNABORTED:
+ constant = "ECONNABORTED"
+ case unix.ECONNREFUSED:
+ constant = "ECONNREFUSED"
+ case unix.ECONNRESET:
+ constant = "ECONNRESET"
+ case unix.EDEADLOCK:
+ constant = "EDEADLOCK"
+ case unix.EDESTADDRREQ:
+ constant = "EDESTADDRREQ"
+ case unix.EDOM:
+ constant = "EDOM"
+ case unix.EDOTDOT:
+ constant = "EDOTDOT"
+ case unix.EDQUOT:
+ constant = "EDQUOT"
+ case unix.EEXIST:
+ constant = "EEXIST"
+ case unix.EFAULT:
+ constant = "EFAULT"
+ case unix.EFBIG:
+ constant = "EFBIG"
+ case unix.EHOSTDOWN:
+ constant = "EHOSTDOWN"
+ case unix.EHOSTUNREACH:
+ constant = "EHOSTUNREACH"
+ case unix.EIDRM:
+ constant = "EIDRM"
+ case unix.EILSEQ:
+ constant = "EILSEQ"
+ case unix.EINPROGRESS:
+ constant = "EINPROGRESS"
+ case unix.EINTR:
+ constant = "EINTR"
+ case unix.EINVAL:
+ constant = "EINVAL"
+ case unix.EIO:
+ constant = "EIO"
+ case unix.EISCONN:
+ constant = "EISCONN"
+ case unix.EISDIR:
+ constant = "EISDIR"
+ case unix.EISNAM:
+ constant = "EISNAM"
+ case unix.EKEYEXPIRED:
+ constant = "EKEYEXPIRED"
+ case unix.EKEYREJECTED:
+ constant = "EKEYREJECTED"
+ case unix.EKEYREVOKED:
+ constant = "EKEYREVOKED"
+ case unix.EL2HLT:
+ constant = "EL2HLT"
+ case unix.EL2NSYNC:
+ constant = "EL2NSYNC"
+ case unix.EL3HLT:
+ constant = "EL3HLT"
+ case unix.EL3RST:
+ constant = "EL3RST"
+ case unix.ELIBACC:
+ constant = "ELIBACC"
+ case unix.ELIBBAD:
+ constant = "ELIBBAD"
+ case unix.ELIBEXEC:
+ constant = "ELIBEXEC"
+ case unix.ELIBMAX:
+ constant = "ELIBMAX"
+ case unix.ELIBSCN:
+ constant = "ELIBSCN"
+ case unix.ELNRNG:
+ constant = "ELNRNG"
+ case unix.ELOOP:
+ constant = "ELOOP"
+ case unix.EMEDIUMTYPE:
+ constant = "EMEDIUMTYPE"
+ case unix.EMFILE:
+ constant = "EMFILE"
+ case unix.EMLINK:
+ constant = "EMLINK"
+ case unix.EMSGSIZE:
+ constant = "EMSGSIZE"
+ case unix.EMULTIHOP:
+ constant = "EMULTIHOP"
+ case unix.ENAMETOOLONG:
+ constant = "ENAMETOOLONG"
+ case unix.ENAVAIL:
+ constant = "ENAVAIL"
+ case unix.ENETDOWN:
+ constant = "ENETDOWN"
+ case unix.ENETRESET:
+ constant = "ENETRESET"
+ case unix.ENETUNREACH:
+ constant = "ENETUNREACH"
+ case unix.ENFILE:
+ constant = "ENFILE"
+ case unix.ENOANO:
+ constant = "ENOANO"
+ case unix.ENOBUFS:
+ constant = "ENOBUFS"
+ case unix.ENOCSI:
+ constant = "ENOCSI"
+ case unix.ENODATA:
+ constant = "ENODATA"
+ case unix.ENODEV:
+ constant = "ENODEV"
+ case unix.ENOENT:
+ constant = "ENOENT"
+ case unix.ENOEXEC:
+ constant = "ENOEXEC"
+ case unix.ENOKEY:
+ constant = "ENOKEY"
+ case unix.ENOLCK:
+ constant = "ENOLCK"
+ case unix.ENOLINK:
+ constant = "ENOLINK"
+ case unix.ENOMEDIUM:
+ constant = "ENOMEDIUM"
+ case unix.ENOMEM:
+ constant = "ENOMEM"
+ case unix.ENOMSG:
+ constant = "ENOMSG"
+ case unix.ENONET:
+ constant = "ENONET"
+ case unix.ENOPKG:
+ constant = "ENOPKG"
+ case unix.ENOPROTOOPT:
+ constant = "ENOPROTOOPT"
+ case unix.ENOSPC:
+ constant = "ENOSPC"
+ case unix.ENOSR:
+ constant = "ENOSR"
+ case unix.ENOSTR:
+ constant = "ENOSTR"
+ case unix.ENOSYS:
+ constant = "ENOSYS"
+ case unix.ENOTBLK:
+ constant = "ENOTBLK"
+ case unix.ENOTCONN:
+ constant = "ENOTCONN"
+ case unix.ENOTDIR:
+ constant = "ENOTDIR"
+ case unix.ENOTEMPTY:
+ constant = "ENOTEMPTY"
+ case unix.ENOTNAM:
+ constant = "ENOTNAM"
+ case unix.ENOTRECOVERABLE:
+ constant = "ENOTRECOVERABLE"
+ case unix.ENOTSOCK:
+ constant = "ENOTSOCK"
+ case unix.ENOTSUP:
+ constant = "ENOTSUP"
+ case unix.ENOTTY:
+ constant = "ENOTTY"
+ case unix.ENOTUNIQ:
+ constant = "ENOTUNIQ"
+ case unix.ENXIO:
+ constant = "ENXIO"
+ case unix.EOVERFLOW:
+ constant = "EOVERFLOW"
+ case unix.EOWNERDEAD:
+ constant = "EOWNERDEAD"
+ case unix.EPERM:
+ constant = "EPERM"
+ case unix.EPFNOSUPPORT:
+ constant = "EPFNOSUPPORT"
+ case unix.EPIPE:
+ constant = "EPIPE"
+ case unix.EPROTO:
+ constant = "EPROTO"
+ case unix.EPROTONOSUPPORT:
+ constant = "EPROTONOSUPPORT"
+ case unix.EPROTOTYPE:
+ constant = "EPROTOTYPE"
+ case unix.ERANGE:
+ constant = "ERANGE"
+ case unix.EREMCHG:
+ constant = "EREMCHG"
+ case unix.EREMOTE:
+ constant = "EREMOTE"
+ case unix.EREMOTEIO:
+ constant = "EREMOTEIO"
+ case unix.ERESTART:
+ constant = "ERESTART"
+ case unix.ERFKILL:
+ constant = "ERFKILL"
+ case unix.EROFS:
+ constant = "EROFS"
+ case unix.ESHUTDOWN:
+ constant = "ESHUTDOWN"
+ case unix.ESOCKTNOSUPPORT:
+ constant = "ESOCKTNOSUPPORT"
+ case unix.ESPIPE:
+ constant = "ESPIPE"
+ case unix.ESRCH:
+ constant = "ESRCH"
+ case unix.ESRMNT:
+ constant = "ESRMNT"
+ case unix.ESTALE:
+ constant = "ESTALE"
+ case unix.ESTRPIPE:
+ constant = "ESTRPIPE"
+ case unix.ETIME:
+ constant = "ETIME"
+ case unix.ETIMEDOUT:
+ constant = "ETIMEDOUT"
+ case unix.ETOOMANYREFS:
+ constant = "ETOOMANYREFS"
+ case unix.ETXTBSY:
+ constant = "ETXTBSY"
+ case unix.EUCLEAN:
+ constant = "EUCLEAN"
+ case unix.EUNATCH:
+ constant = "EUNATCH"
+ case unix.EUSERS:
+ constant = "EUSERS"
+ case unix.EXDEV:
+ constant = "EXDEV"
+ case unix.EXFULL:
+ constant = "EXFULL"
+ }
+
+ msg := syscall.Errno(code).Error()
+ p.PrintColour(ColourRed, "%d %s (%s)", arg.Int()+1, constant, msg)
+}
diff --git a/printer/hexdump.go b/printer/hexdump.go
new file mode 100644
index 0000000..bfcd172
--- /dev/null
+++ b/printer/hexdump.go
@@ -0,0 +1,54 @@
+package printer
+
+import "strings"
+
+const dumpWidth = 16
+
+func (p *Printer) HexDump(addr uintptr, data []byte, indent int) {
+
+ var truncatedFrom uintptr
+ if p.maxHexDumpLen > 0 && len(data) > p.maxHexDumpLen {
+ truncatedFrom = uintptr(len(data))
+ data = data[:p.maxHexDumpLen]
+ }
+
+ startAddr := addr - (addr % dumpWidth)
+ endAddr := addr + uintptr(len(data))
+ if endAddr%dumpWidth > 0 {
+ endAddr += dumpWidth - (endAddr % dumpWidth)
+ }
+ p.Print(strings.Repeat(" ", indent))
+ p.Print("(see below hexdump)")
+ p.NewLine(indent)
+ p.Print(" ")
+ for i := 0; i < dumpWidth; i++ {
+ p.PrintDim("%02x ", i)
+ }
+ for i := startAddr; i < endAddr; i += dumpWidth {
+ p.NewLine(indent)
+ p.PrintDim("%16x: ", i)
+ for j := 0; j < dumpWidth; j++ {
+ local := (i + uintptr(j)) - addr
+ if i+uintptr(j) < addr || local >= uintptr(len(data)) {
+ p.PrintDim(".. ")
+ } else {
+ p.PrintColour(ColourRed, "%02x ", data[local])
+ }
+ }
+ for j := 0; j < dumpWidth; j++ {
+ local := (i + uintptr(j)) - addr
+ if i+uintptr(j) < addr || local >= uintptr(len(data)) {
+ p.PrintDim(".")
+ } else {
+ c := data[local]
+ if c < 32 || c > 126 {
+ c = '.'
+ }
+ p.PrintColour(ColourBlue, "%c", c)
+ }
+ }
+ }
+ if truncatedFrom > 0 {
+ p.PrintDim("\n... (truncated from %d bytes -> %d bytes) ...", truncatedFrom, p.maxHexDumpLen)
+ }
+}
diff --git a/printer/object.go b/printer/object.go
new file mode 100644
index 0000000..e624bb2
--- /dev/null
+++ b/printer/object.go
@@ -0,0 +1,38 @@
+package printer
+
+import (
+ "github.com/liamg/grace/tracer"
+)
+
+func (p *Printer) printObject(obj *tracer.Object, colour Colour, exit bool, count int, indent int) int {
+ if obj == nil {
+ p.PrintDim("NULL")
+ return count
+ }
+ prevIndent := indent
+ indent += indentSize
+ p.PrintColour(colour, "%s{", obj.Name)
+
+ for i, prop := range obj.Properties {
+ if p.multiline {
+ p.NewLine(indent)
+ }
+ colour := colours[i%len(colours)]
+ p.PrintDim("%s", prop.Name())
+ p.PrintDim(": ")
+ count += p.PrintArgValue(&prop, colour, exit, count, indent+indentSize)
+ if i < len(obj.Properties)-1 {
+ p.PrintDim(", ")
+ }
+ if p.maxObjectProperties > 0 && count >= p.maxObjectProperties {
+ p.PrintDim("...")
+ break
+ }
+ count++
+ }
+ if p.multiline {
+ p.NewLine(prevIndent)
+ }
+ p.PrintColour(colour, "}")
+ return count
+}
diff --git a/printer/printer.go b/printer/printer.go
new file mode 100644
index 0000000..13dc7fe
--- /dev/null
+++ b/printer/printer.go
@@ -0,0 +1,89 @@
+package printer
+
+import (
+ "fmt"
+ "io"
+)
+
+type Printer struct {
+ w io.Writer
+ useColours bool
+ maxStringLen int
+ hexDumpLongStrings bool
+ maxHexDumpLen int
+ maxObjectProperties int
+ colourIndex int
+ argProgress int
+ extraNewLine bool
+ multiline bool
+}
+
+func New(w io.Writer) *Printer {
+ return &Printer{
+ w: w,
+ useColours: true,
+ maxStringLen: 32,
+ hexDumpLongStrings: true,
+ maxHexDumpLen: 4096,
+ maxObjectProperties: 2,
+ }
+}
+
+const indentSize = 4
+
+func (p *Printer) SetUseColours(useColours bool) {
+ p.useColours = useColours
+}
+
+func (p *Printer) SetMaxStringLen(maxStringLen int) {
+ p.maxStringLen = maxStringLen
+}
+
+func (p *Printer) SetHexDumpLongStrings(hexDumpLongStrings bool) {
+ p.hexDumpLongStrings = hexDumpLongStrings
+}
+
+func (p *Printer) SetMaxHexDumpLen(maxHexDumpLen int) {
+ p.maxHexDumpLen = maxHexDumpLen
+}
+
+func (p *Printer) SetMaxObjectProperties(maxObjectProperties int) {
+ p.maxObjectProperties = maxObjectProperties
+}
+
+func (p *Printer) SetExtraNewLine(extraNewLine bool) {
+ p.extraNewLine = extraNewLine
+}
+
+func (p *Printer) Print(format string, args ...interface{}) {
+ _, _ = fmt.Fprintf(p.w, format, args...)
+}
+
+func (p *Printer) PrintDim(format string, args ...interface{}) {
+ p.PrintColour(2, format, args...)
+}
+
+func (p *Printer) PrintColour(colour Colour, format string, args ...interface{}) {
+ if p.useColours {
+ p.Print("\x1b[%dm", colour)
+ }
+ p.Print(format, args...)
+ if p.useColours {
+ p.Print("\x1b[0m")
+ }
+}
+
+func (p *Printer) SetMultiLine(multiline bool) {
+ p.multiline = multiline
+}
+
+func (p *Printer) PrintProcessExit(i int) {
+ colour := ColourGreen
+ if i != 0 {
+ colour = ColourRed
+ }
+ if p.multiline {
+ p.Print("\n")
+ }
+ p.PrintColour(colour, "\nProcess exited with status %d\n", i)
+}
diff --git a/printer/signal.go b/printer/signal.go
new file mode 100644
index 0000000..178643c
--- /dev/null
+++ b/printer/signal.go
@@ -0,0 +1,20 @@
+package printer
+
+import (
+ "fmt"
+ "syscall"
+)
+
+func (p *Printer) PrintSignal(signal syscall.Signal) {
+ p.PrintColour(ColourYellow, "--> SIGNAL: %s <--\n", signalToString(signal))
+}
+
+func signalToString(signal syscall.Signal) string {
+ switch signal {
+ // TODO: more signals
+ case syscall.SIGURG:
+ return "SIGURG"
+ default:
+ return fmt.Sprintf("signal %d", signal)
+ }
+}
diff --git a/printer/syscall.go b/printer/syscall.go
new file mode 100644
index 0000000..90721cc
--- /dev/null
+++ b/printer/syscall.go
@@ -0,0 +1,53 @@
+package printer
+
+import "github.com/liamg/grace/tracer"
+
+func (p *Printer) PrintSyscallEnter(syscall *tracer.Syscall) {
+ p.colourIndex = 0
+ p.argProgress = 0
+ if syscall.Unknown() {
+ p.PrintColour(ColourRed, syscall.Name())
+ } else {
+ p.PrintColour(ColourDefault, syscall.Name())
+ }
+ p.printRemainingArgs(syscall, false)
+}
+
+func (p *Printer) PrintSyscallExit(syscall *tracer.Syscall) {
+ p.printRemainingArgs(syscall, true)
+ p.PrintDim(" = ")
+ ret := syscall.Return()
+ p.PrintArgValue(&ret, ColourWhite, true, 0, 0)
+ p.Print("\n")
+ if p.extraNewLine {
+ p.Print("\n")
+ }
+}
+
+func (p *Printer) printRemainingArgs(syscall *tracer.Syscall, exit bool) {
+ if !exit {
+ p.PrintDim("(")
+ }
+ remaining := syscall.Args()[p.argProgress:]
+ for i, arg := range remaining {
+ if !arg.Known() {
+ break
+ }
+ if p.argProgress == 0 && p.multiline {
+ p.Print("\n")
+ }
+ p.PrintArg(arg, exit)
+ if i < len(remaining)-1 {
+ p.PrintDim(", ")
+ }
+ p.argProgress++
+ if p.multiline {
+ p.Print("\n")
+ }
+ }
+
+ if (exit && len(remaining) > 0) || (!exit && p.argProgress == len(syscall.Args())) {
+ p.PrintDim(")")
+ }
+
+}
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..af368ea
Binary files /dev/null and b/screenshot.png differ
diff --git a/testdata/pipe/main.go b/testdata/pipe/main.go
new file mode 100644
index 0000000..d82da37
--- /dev/null
+++ b/testdata/pipe/main.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+ "fmt"
+ "syscall"
+)
+
+func main() {
+ fds := make([]int, 2)
+ _ = syscall.Pipe2(fds, 0)
+ fmt.Println(fds[0], fds[1])
+}
diff --git a/tracer/annotations.go b/tracer/annotations.go
new file mode 100644
index 0000000..a3aa53a
--- /dev/null
+++ b/tracer/annotations.go
@@ -0,0 +1,132 @@
+package tracer
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+)
+
+func annotateFd(arg *Arg, pid int) {
+ switch int(arg.Raw()) {
+ case syscall.Stdin:
+ arg.annotation = "stdin"
+ case syscall.Stdout:
+ arg.annotation = "stdout"
+ case syscall.Stderr:
+ arg.annotation = "stderr"
+ default:
+ if path, err := os.Readlink(fmt.Sprintf("/proc/%d/fd/%d", pid, arg.Raw())); err == nil {
+ arg.annotation = path
+ } else if int32(arg.Raw()) == unix.AT_FDCWD {
+ arg.annotation = "AT_FDCWD"
+ arg.replace = true
+ }
+ }
+}
+
+func annotateAccMode(arg *Arg, _ int) {
+ var joins []string
+ if arg.Raw() == unix.F_OK {
+ joins = append(joins, "F_OK")
+ } else {
+ if arg.Raw()&unix.R_OK > 0 {
+ joins = append(joins, "R_OK")
+ }
+ if arg.Raw()&unix.W_OK > 0 {
+ joins = append(joins, "W_OK")
+ }
+ if arg.Raw()&unix.X_OK > 0 {
+ joins = append(joins, "X_OK")
+ }
+ }
+
+ arg.annotation = strings.Join(joins, "|")
+ arg.replace = arg.annotation != ""
+}
+
+func annotateNull(arg *Arg, _ int) {
+ if arg.Raw() == 0 {
+ arg.annotation = "NULL"
+ arg.replace = true
+ }
+}
+
+func annotateOpenFlags(arg *Arg, pid int) {
+
+ mapFlags := map[int]string{
+ unix.O_APPEND: "O_APPEND",
+ unix.O_ASYNC: "O_ASYNC",
+ unix.O_CLOEXEC: "O_CLOEXEC",
+ unix.O_CREAT: "O_CREAT",
+ unix.O_DIRECT: "O_DIRECT",
+ unix.O_DIRECTORY: "O_DIRECTORY",
+ unix.O_DSYNC: "O_DSYNC",
+ unix.O_EXCL: "O_EXCL",
+ unix.O_NOATIME: "O_NOATIME",
+ unix.O_NOCTTY: "O_NOCTTY",
+ unix.O_NOFOLLOW: "O_NOFOLLOW",
+ unix.O_NONBLOCK: "O_NONBLOCK",
+ unix.O_PATH: "O_PATH",
+ unix.O_SYNC: "O_SYNC",
+ unix.O_TMPFILE: "O_TMPFILE",
+ unix.O_TRUNC: "O_TRUNC",
+ unix.O_RDONLY: "O_RDONLY",
+ }
+
+ var joins []string
+
+ for flag, name := range mapFlags {
+ if (int(arg.Raw())&flag != 0) || (int(arg.Raw()) == flag) {
+ joins = append(joins, name)
+ }
+ }
+
+ arg.annotation = strings.Join(joins, "|")
+ arg.replace = arg.annotation != ""
+}
+
+func annotateWhence(arg *Arg, pid int) {
+ switch int(arg.Raw()) {
+ case unix.SEEK_SET:
+ arg.annotation = "SEEK_SET"
+ case unix.SEEK_CUR:
+ arg.annotation = "SEEK_CUR"
+ case unix.SEEK_END:
+ arg.annotation = "SEEK_END"
+ case unix.SEEK_DATA:
+ arg.annotation = "SEEK_DATA"
+ case unix.SEEK_HOLE:
+ arg.annotation = "SEEK_HOLE"
+ }
+ arg.replace = arg.annotation != ""
+}
+
+func annotateProt(arg *Arg, pid int) {
+ if arg.Raw() == unix.PROT_NONE {
+ arg.annotation = "PROT_NONE"
+ arg.replace = true
+ return
+ }
+ var joins []string
+ if arg.Raw()&unix.PROT_READ > 0 {
+ joins = append(joins, "PROT_READ")
+ }
+ if arg.Raw()&unix.PROT_WRITE > 0 {
+ joins = append(joins, "PROT_WRITE")
+ }
+ if arg.Raw()&unix.PROT_EXEC > 0 {
+ joins = append(joins, "PROT_EXEC")
+ }
+ if arg.Raw()&unix.PROT_GROWSUP > 0 {
+ joins = append(joins, "PROT_GROWSUP")
+ }
+ if arg.Raw()&unix.PROT_GROWSDOWN > 0 {
+ joins = append(joins, "PROT_GROWSDOWN")
+ }
+
+ arg.annotation = strings.Join(joins, "|")
+ arg.replace = arg.annotation != ""
+}
diff --git a/tracer/args.go b/tracer/args.go
new file mode 100644
index 0000000..e8746c6
--- /dev/null
+++ b/tracer/args.go
@@ -0,0 +1,140 @@
+package tracer
+
+type ArgMetadata struct {
+ Name string
+ Type ArgType
+ Annotator func(arg *Arg, pid int)
+ CountFrom CountLocation
+ Optional bool
+ Destination bool
+ FixedCount int
+}
+
+type CountLocation uint8
+
+const (
+ CountLocationNone CountLocation = iota
+ CountLocationNext
+ CountLocationResult
+ CountLocationNullTerminator
+ CountLocationFixed
+)
+
+type ReturnMetadata ArgMetadata
+
+type ArgType int
+
+const (
+ ArgTypeUnknown ArgType = iota
+ ArgTypeData
+ ArgTypeInt
+ ArgTypeStat
+ ArgTypeLong
+ ArgTypeAddress
+ ArgTypeUnsignedInt
+ ArgTypeUnsignedLong
+ ArgTypePollFdArray
+ ArgTypeObject
+ ArgTypeErrorCode
+ ArgTypeSigAction
+ ArgTypeIovecArray
+ ArgTypeIntArray
+)
+
+type Arg struct {
+ name string
+ t ArgType
+ raw uintptr
+ data []byte
+ annotation string
+ replace bool // replace value output with annotation
+ bitSize int
+ obj *Object
+ array []Arg
+ known bool
+}
+
+type Object struct {
+ Name string
+ Properties []Arg
+}
+
+func (s Arg) Known() bool {
+ return s.known
+}
+
+func (s Arg) Name() string {
+ return s.name
+}
+
+func (s Arg) Type() ArgType {
+ return s.t
+}
+
+func (s Arg) Raw() uintptr {
+ return s.raw
+}
+
+func (s Arg) Int() int {
+ switch s.t {
+ case ArgTypeLong:
+ switch s.bitSize {
+ case 32:
+ return int(int32(s.raw))
+ case 64:
+ return int(int64(s.raw))
+ }
+ case ArgTypeInt, ArgTypeUnknown:
+ // an int is 32-bit on both 32-bit and 64-bit linux
+ return int(int32(s.raw))
+ }
+ return int(s.raw)
+}
+
+func (s Arg) Data() []byte {
+ return s.data
+}
+
+func (s Arg) Annotation() string {
+ return s.annotation
+}
+
+func (s Arg) ReplaceValueWithAnnotation() bool {
+ return s.replace
+}
+
+func (s Arg) Object() *Object {
+ return s.obj
+}
+
+func (s Arg) Array() []Arg {
+ return s.array
+}
+
+func processArgument(raw uintptr, next uintptr, ret uintptr, metadata ArgMetadata, pid int, exit bool) (*Arg, error) {
+ arg := &Arg{
+ name: metadata.Name,
+ t: metadata.Type,
+ raw: raw,
+ bitSize: bitSize,
+ known: true,
+ }
+
+ // if we're on the syscall enter and the argument is a pointer for a destination, we don't know the value yet
+ if !exit && metadata.Destination {
+ arg.known = false
+ return arg, nil
+ }
+
+ // process the argument data into something meaningful
+ if err := handleType(arg, metadata, raw, next, ret, pid); err != nil {
+ return nil, err
+ }
+
+ // always apply annotations
+ if metadata.Annotator != nil {
+ metadata.Annotator(arg, pid)
+ }
+
+ return arg, nil
+}
diff --git a/tracer/decode.go b/tracer/decode.go
new file mode 100644
index 0000000..5e5b840
--- /dev/null
+++ b/tracer/decode.go
@@ -0,0 +1,83 @@
+package tracer
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+)
+
+func decodeStruct(memory []byte, target interface{}) error {
+
+ sType := reflect.TypeOf(target)
+ if sType.Kind() != reflect.Ptr {
+ return errors.New("target must be a pointer")
+ }
+
+ var index uintptr
+
+ sPtrValue := reflect.ValueOf(target)
+ sValue := sPtrValue.Elem()
+
+ switch sValue.Kind() {
+ case reflect.Struct:
+ for i := 0; i < sValue.Type().NumField(); i++ {
+ size := sValue.Type().Field(i).Type.Size()
+ raw := memory[index : index+size]
+ if err := decodeAnonymous(sValue.Field(i), raw); err != nil {
+ return err
+ }
+ index += size
+ }
+ default:
+ return errors.New("target must be a pointer to a struct")
+ }
+
+ return nil
+}
+
+func decodeAnonymous(target reflect.Value, raw []byte) error {
+
+ if target.Kind() == reflect.Ptr {
+ target = target.Elem()
+ }
+
+ switch target.Kind() {
+ case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint, reflect.Uintptr:
+ target.SetUint(decodeUint(raw))
+ case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
+ target.SetInt(decodeInt(raw))
+ case reflect.String:
+ target.SetString(string(raw))
+ case reflect.Struct:
+ if err := decodeStruct(raw, target.Addr().Interface()); err != nil {
+ return err
+ }
+ case reflect.Array, reflect.Slice:
+ var index uintptr
+ for i := 0; i < target.Len(); i++ {
+ memory := raw[index : index+target.Type().Elem().Size()]
+ if err := decodeAnonymous(target.Index(i), memory); err != nil {
+ return err
+ }
+ index += target.Type().Elem().Size()
+ }
+ default:
+ return fmt.Errorf("unsupported kind for field '%s': %s", target.String(), target.Kind().String())
+ }
+ return nil
+}
+
+func decodeUint(raw []byte) uint64 {
+ var output uint64
+ for i := 0; i < len(raw); i++ {
+ output |= uint64(raw[i]) << uint(i*8)
+ }
+ return output
+}
+func decodeInt(raw []byte) int64 {
+ var output int64
+ for i := 0; i < len(raw); i++ {
+ output |= int64(raw[i]) << uint(i*8)
+ }
+ return output
+}
diff --git a/tracer/iovec.go b/tracer/iovec.go
new file mode 100644
index 0000000..c8a8a23
--- /dev/null
+++ b/tracer/iovec.go
@@ -0,0 +1,36 @@
+package tracer
+
+type iovec struct {
+ Base uintptr /* Starting address */
+ Len uint /* Number of bytes to transfer */
+}
+
+func convertIovecs(vecs []iovec) []Arg {
+ var output []Arg
+ for _, fd := range vecs {
+ output = append(output, convertIovec(fd))
+ }
+ return output
+}
+
+func convertIovec(vec iovec) Arg {
+ return Arg{
+ t: ArgTypeObject,
+ obj: &Object{
+ Name: "iovec",
+ Properties: []Arg{
+ {
+ name: "base",
+ t: ArgTypeAddress,
+ raw: vec.Base,
+ },
+ {
+ name: "len",
+ t: ArgTypeUnsignedInt,
+ raw: uintptr(vec.Len),
+ },
+ },
+ },
+ known: true,
+ }
+}
diff --git a/tracer/poll.go b/tracer/poll.go
new file mode 100644
index 0000000..b325707
--- /dev/null
+++ b/tracer/poll.go
@@ -0,0 +1,42 @@
+package tracer
+
+type pollfd struct {
+ Fd int /* file descriptor */
+ Events uint16 /* events requested for polling */
+ REvents uint32 /* events that occurred during polling */
+}
+
+func convertPollFds(fds []pollfd) []Arg {
+ var output []Arg
+ for _, fd := range fds {
+ output = append(output, convertPollFd(fd))
+ }
+ return output
+}
+
+func convertPollFd(fd pollfd) Arg {
+ return Arg{
+ t: ArgTypeObject,
+ obj: &Object{
+ Name: "pollfd",
+ Properties: []Arg{
+ {
+ name: "fd",
+ t: ArgTypeInt,
+ raw: uintptr(fd.Fd),
+ },
+ {
+ name: "events",
+ t: ArgTypeInt,
+ raw: uintptr(fd.Events),
+ },
+ {
+ name: "revents",
+ t: ArgTypeInt,
+ raw: uintptr(fd.REvents),
+ },
+ },
+ },
+ known: true,
+ }
+}
diff --git a/tracer/sys.go b/tracer/sys.go
new file mode 100644
index 0000000..7ee360a
--- /dev/null
+++ b/tracer/sys.go
@@ -0,0 +1,96 @@
+package tracer
+
+import "fmt"
+
+type Syscall struct {
+ pid int
+ number int
+ rawArgs [6]uintptr
+ args []Arg
+ rawRet uintptr
+ ret Arg
+ unknown bool
+}
+
+type SyscallMetadata struct {
+ Name string
+ Args []ArgMetadata
+ ReturnValue ReturnMetadata
+}
+
+func LookupSyscall(number int) (*SyscallMetadata, error) {
+ meta, ok := sysMap[number]
+ if !ok {
+ return nil, fmt.Errorf("unknown syscall %d", number)
+ }
+ return &meta, nil
+}
+
+func (s *Syscall) Number() int {
+ return s.number
+}
+
+func (s *Syscall) Name() string {
+ meta, ok := sysMap[s.number]
+ if !ok {
+ return fmt.Sprintf("unknown_syscall_%d", s.number)
+ }
+ return meta.Name
+}
+
+func (s *Syscall) Args() []Arg {
+ return s.args
+}
+
+func (s *Syscall) Return() Arg {
+ return s.ret
+}
+
+func (s *Syscall) Unknown() bool {
+ return s.unknown
+}
+
+func (s *Syscall) populate(exit bool) error {
+ meta, ok := sysMap[s.number]
+ if !ok {
+ s.unknown = true
+ }
+ ret, err := processArgument(s.rawRet, 0, 0, ArgMetadata(meta.ReturnValue), s.pid, exit)
+ if err != nil {
+ return fmt.Errorf("failed to set return value of syscall %s (%d): %w", meta.Name, s.number, err)
+ }
+ s.ret = *ret
+ for i, argMeta := range meta.Args {
+ if exit && !argMeta.Destination && i < len(s.args) {
+ continue
+ }
+
+ var next uintptr
+ if i < len(meta.Args)-1 {
+ next = s.rawArgs[i+1]
+ }
+ arg, err := processArgument(s.rawArgs[i], next, s.rawRet, argMeta, s.pid, exit)
+ if err != nil {
+ return fmt.Errorf("failed to set argument %d (%s) of syscall %s (%d): %w", i, argMeta.Name, meta.Name, s.number, err)
+ }
+ if i >= len(s.args) {
+ s.args = append(s.args, *arg)
+ } else {
+ s.args[i] = *arg
+ }
+ }
+
+ // strip off trailing optional args if they have no value
+ var lastIndex int
+ for i, arg := range s.args {
+ meta := meta.Args[i]
+ if !meta.Optional || arg.Raw() > 0 {
+ lastIndex = i
+ }
+ }
+ if lastIndex < len(s.args)-1 {
+ s.args = s.args[:lastIndex+1]
+ }
+
+ return nil
+}
diff --git a/tracer/sys_amd64.go b/tracer/sys_amd64.go
new file mode 100644
index 0000000..363bd9b
--- /dev/null
+++ b/tracer/sys_amd64.go
@@ -0,0 +1,636 @@
+//go:build amd64
+
+package tracer
+
+import (
+ "strings"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+)
+
+const bitSize = 64
+
+// useful info: https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md
+
+func parseSyscall(regs *syscall.PtraceRegs) *Syscall {
+ return &Syscall{
+ number: int(regs.Orig_rax),
+ rawArgs: [6]uintptr{
+ uintptr(regs.Rdi),
+ uintptr(regs.Rsi),
+ uintptr(regs.Rdx),
+ uintptr(regs.R10),
+ uintptr(regs.R8),
+ uintptr(regs.R9),
+ },
+ rawRet: uintptr(regs.Rax),
+ }
+}
+
+var (
+ sysMap = map[int]SyscallMetadata{
+ unix.SYS_READ: {
+ Name: "read",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeInt,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "buf",
+ Type: ArgTypeData,
+ CountFrom: CountLocationResult,
+ Destination: true,
+ },
+ {
+ Name: "count",
+ Type: ArgTypeUnsignedInt,
+ },
+ },
+ },
+ unix.SYS_WRITE: {
+ Name: "write",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeInt,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "buf",
+ Type: ArgTypeData,
+ CountFrom: CountLocationNext,
+ },
+ {
+ Name: "count",
+ Type: ArgTypeUnsignedInt,
+ },
+ },
+ },
+ unix.SYS_OPEN: {
+ Name: "open",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeInt,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "filename",
+ Type: ArgTypeData,
+ CountFrom: CountLocationNullTerminator,
+ },
+ {
+ Name: "flags",
+ Type: ArgTypeInt,
+ Annotator: annotateOpenFlags,
+ },
+ {
+ Name: "mode",
+ Type: ArgTypeInt,
+ Annotator: annotateAccMode,
+ },
+ },
+ },
+ unix.SYS_CLOSE: {
+ Name: "close",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ },
+ },
+ unix.SYS_STAT: {
+ Name: "stat",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "filename",
+ Type: ArgTypeData,
+ CountFrom: CountLocationNullTerminator,
+ },
+ {
+ Name: "stat",
+ Type: ArgTypeStat,
+ Destination: true,
+ },
+ },
+ },
+ unix.SYS_FSTAT: {
+ Name: "fstat",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "stat",
+ Type: ArgTypeStat,
+ Destination: true,
+ },
+ },
+ },
+ unix.SYS_LSTAT: {
+ Name: "lstat",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "filename",
+ Type: ArgTypeData,
+ CountFrom: CountLocationNullTerminator,
+ },
+ {
+ Name: "stat",
+ Type: ArgTypeStat,
+ Destination: true,
+ },
+ },
+ },
+ unix.SYS_POLL: {
+ Name: "poll",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "ufds",
+ Type: ArgTypePollFdArray,
+ Destination: true,
+ },
+ {
+ Name: "nfds",
+ Type: ArgTypeUnsignedInt,
+ },
+ {
+ Name: "timeout",
+ Type: ArgTypeInt,
+ },
+ },
+ },
+ unix.SYS_LSEEK: {
+ Name: "lseek",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeInt,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "offset",
+ Type: ArgTypeInt,
+ },
+ {
+ Name: "whence",
+ Type: ArgTypeUnsignedInt,
+ Annotator: annotateWhence,
+ },
+ },
+ },
+ unix.SYS_MMAP: {
+ Name: "mmap",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeAddress,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "addr",
+ Type: ArgTypeAddress,
+ Annotator: annotateNull,
+ },
+ {
+ Name: "len",
+ Type: ArgTypeUnsignedLong,
+ },
+ {
+ Name: "prot",
+ Type: ArgTypeUnsignedLong,
+ Annotator: annotateProt,
+ },
+ {
+ Name: "flags",
+ Type: ArgTypeUnsignedLong,
+ Annotator: func(arg *Arg, _ int) {
+ var joins []string
+
+ switch arg.Raw() & 0x3 {
+ case unix.MAP_SHARED:
+ joins = append(joins, "MAP_SHARED")
+ case unix.MAP_PRIVATE:
+ joins = append(joins, "MAP_PRIVATE")
+ case unix.MAP_SHARED_VALIDATE:
+ joins = append(joins, "MAP_SHARED_VALIDATE")
+ }
+
+ mapConsts := map[int]string{
+ unix.MAP_32BIT: "MAP_32BIT",
+ unix.MAP_ANONYMOUS: "MAP_ANONYMOUS",
+ unix.MAP_DENYWRITE: "MAP_DENYWRITE",
+ unix.MAP_EXECUTABLE: "MAP_EXECUTABLE",
+ unix.MAP_FILE: "MAP_FILE",
+ unix.MAP_FIXED: "MAP_FIXED",
+ unix.MAP_FIXED_NOREPLACE: "MAP_FIXED_NOREPLACE",
+ unix.MAP_GROWSDOWN: "MAP_GROWSDOWN",
+ unix.MAP_HUGETLB: "MAP_HUGETLB",
+ 21 << unix.MAP_HUGE_SHIFT: "MAP_HUGE_2MB",
+ 30 << unix.MAP_HUGE_SHIFT: "MAP_HUGE_1GB",
+ unix.MAP_LOCKED: "MAP_LOCKED",
+ unix.MAP_NONBLOCK: "MAP_NONBLOCK",
+ unix.MAP_NORESERVE: "MAP_NORESERVE",
+ unix.MAP_POPULATE: "MAP_POPULATE",
+ unix.MAP_STACK: "MAP_STACK",
+ unix.MAP_SYNC: "MAP_SYNC",
+ }
+
+ for flag, str := range mapConsts {
+ if arg.Raw()&uintptr(flag) > 0 {
+ joins = append(joins, str)
+ }
+ }
+ arg.annotation = strings.Join(joins, "|")
+ arg.replace = arg.annotation != ""
+ },
+ },
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "off",
+ Type: ArgTypeUnsignedLong,
+ },
+ },
+ },
+ unix.SYS_MPROTECT: {
+ Name: "mprotect",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "start",
+ Type: ArgTypeAddress,
+ Annotator: annotateNull,
+ },
+ {
+ Name: "len",
+ Type: ArgTypeUnsignedInt,
+ },
+ {
+ Name: "prot",
+ Type: ArgTypeUnsignedLong,
+ Annotator: annotateProt,
+ },
+ },
+ },
+ unix.SYS_MUNMAP: {
+ Name: "munmap",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "start",
+ Type: ArgTypeAddress,
+ Annotator: annotateNull,
+ },
+ {
+ Name: "len",
+ Type: ArgTypeUnsignedInt,
+ },
+ },
+ },
+ unix.SYS_BRK: {
+ Name: "brk",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeAddress,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "brk",
+ Type: ArgTypeAddress,
+ Annotator: annotateNull,
+ },
+ },
+ },
+ unix.SYS_RT_SIGACTION: {
+ Name: "rt_sigaction",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "signum",
+ Type: ArgTypeInt,
+ },
+ {
+ Name: "act",
+ Type: ArgTypeSigAction,
+ },
+ {
+ Name: "oldact",
+ Type: ArgTypeSigAction,
+ Destination: true, // TODO: is this correct?
+ },
+ },
+ },
+ unix.SYS_RT_SIGPROCMASK: {
+ Name: "rt_sigprocmask",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "how",
+ Type: ArgTypeInt,
+ Annotator: annotateSigProcMaskFlags,
+ },
+ {
+ Name: "set",
+ Type: ArgTypeAddress,
+ Destination: true,
+ },
+ {
+ Name: "oldset",
+ Type: ArgTypeAddress,
+ Destination: true,
+ },
+ },
+ },
+ unix.SYS_RT_SIGRETURN: {
+ Name: "rt_sigreturn",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "__unused",
+ Type: ArgTypeLong,
+ },
+ },
+ },
+ unix.SYS_IOCTL: {
+ Name: "ioctl",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "request",
+ Type: ArgTypeInt,
+ },
+ {
+ Name: "argp",
+ Type: ArgTypeAddress,
+ },
+ },
+ },
+ unix.SYS_PREAD64: {
+ Name: "pread64",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeInt,
+ },
+ Args: []ArgMetadata{
+
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "buf",
+ Type: ArgTypeData,
+ CountFrom: CountLocationNext,
+ Destination: true,
+ },
+ {
+ Name: "count",
+ Type: ArgTypeInt,
+ },
+ {
+ Name: "offset",
+ Type: ArgTypeInt,
+ },
+ },
+ },
+ unix.SYS_PWRITE64: {
+ Name: "pwrite64",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeInt,
+ },
+ Args: []ArgMetadata{
+
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "buf",
+ Type: ArgTypeData,
+ CountFrom: CountLocationNext,
+ },
+ {
+ Name: "count",
+ Type: ArgTypeInt,
+ },
+ {
+ Name: "offset",
+ Type: ArgTypeInt,
+ },
+ },
+ },
+ unix.SYS_READV: {
+ Name: "readv",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeInt,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "iov",
+ Type: ArgTypeIovecArray,
+ },
+ {
+ Name: "iovcnt",
+ Type: ArgTypeInt,
+ },
+ },
+ },
+ unix.SYS_WRITEV: {
+ Name: "writev",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeInt,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "fd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "iov",
+ Type: ArgTypeIovecArray,
+ },
+ {
+ Name: "iovcnt",
+ Type: ArgTypeInt,
+ },
+ },
+ },
+ unix.SYS_ACCESS: {
+ Name: "access",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "path",
+ Type: ArgTypeData,
+ CountFrom: CountLocationNullTerminator,
+ },
+ {
+ Name: "mode",
+ Type: ArgTypeUnsignedInt,
+ Annotator: annotateAccMode,
+ },
+ },
+ },
+ unix.SYS_PIPE: {
+ Name: "pipe",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "pipefd",
+ Type: ArgTypeIntArray,
+ CountFrom: CountLocationFixed,
+ FixedCount: 2,
+ Destination: true,
+ },
+ },
+ },
+ // <-- progress
+ // TODO: add ReturnValue to everything below here...
+ unix.SYS_PIPE2: {
+ Name: "pipe2",
+ ReturnValue: ReturnMetadata{
+ Type: ArgTypeErrorCode,
+ },
+ Args: []ArgMetadata{
+ {
+ Name: "pipefd",
+ Type: ArgTypeIntArray,
+ CountFrom: CountLocationFixed,
+ FixedCount: 2,
+ Destination: true,
+ },
+ {
+ Name: "flags",
+ Type: ArgTypeInt,
+ // TODO: annotate pipe2 flags
+ },
+ },
+ },
+ unix.SYS_OPENAT: {
+ Name: "openat",
+ Args: []ArgMetadata{
+ {
+ Name: "dfd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "filename",
+ Type: ArgTypeData,
+ CountFrom: CountLocationNullTerminator,
+ },
+ {
+ Name: "flags",
+ Type: ArgTypeInt,
+ Annotator: annotateOpenFlags,
+ },
+ {
+ Name: "mode",
+ Type: ArgTypeInt,
+ Optional: true,
+ Annotator: annotateAccMode,
+ },
+ },
+ },
+ unix.SYS_NEWFSTATAT: {
+ Name: "newfstatat",
+ Args: []ArgMetadata{
+ {
+ Name: "dfd",
+ Type: ArgTypeInt,
+ Annotator: annotateFd,
+ },
+ {
+ Name: "filename",
+ Type: ArgTypeData,
+ CountFrom: CountLocationNullTerminator,
+ },
+ {
+ Name: "statbuf",
+ Type: ArgTypeStat,
+ Destination: true,
+ },
+ {
+ Name: "flag",
+ Type: ArgTypeInt, // TODO: annotate flags
+ },
+ },
+ },
+ unix.SYS_EXIT: {
+ Name: "exit",
+ Args: []ArgMetadata{
+ {
+ Name: "status",
+ Type: ArgTypeInt,
+ },
+ },
+ },
+ unix.SYS_EXIT_GROUP: {
+ Name: "exit_group",
+ Args: []ArgMetadata{
+ {
+ Name: "status",
+ Type: ArgTypeInt,
+ },
+ },
+ },
+ }
+)
diff --git a/tracer/sys_test.go b/tracer/sys_test.go
new file mode 100644
index 0000000..b06885f
--- /dev/null
+++ b/tracer/sys_test.go
@@ -0,0 +1,20 @@
+package tracer
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_SyscallSupport(t *testing.T) {
+ for i := 0; i <= 335; i++ {
+ t.Run(fmt.Sprintf("syscall %d", i), func(t *testing.T) {
+ meta, err := LookupSyscall(i)
+ require.NoError(t, err)
+ require.NotNil(t, meta)
+ assert.NotEmpty(t, meta.Name)
+ })
+ }
+}
diff --git a/tracer/tracer.go b/tracer/tracer.go
new file mode 100644
index 0000000..eecc936
--- /dev/null
+++ b/tracer/tracer.go
@@ -0,0 +1,224 @@
+package tracer
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "os/signal"
+ "runtime"
+ "syscall"
+)
+
+type Tracer struct {
+ handlers struct {
+ syscallExit func(*Syscall)
+ syscallEnter func(*Syscall)
+ signal func(syscall.Signal)
+ processExit func(int)
+ }
+ pid int
+ cmd *exec.Cmd
+ isExit bool
+ lastCall *Syscall
+ lastSignal int
+}
+
+func New(pid int) *Tracer {
+ return &Tracer{
+ pid: pid,
+ }
+}
+
+func FromCommand(suppressOutput bool, command string, args ...string) (*Tracer, error) {
+
+ runtime.LockOSThread()
+
+ cmd := exec.Command(command, args...)
+ cmd.Stdin = os.Stdin
+ if !suppressOutput {
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ }
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Ptrace: true,
+ }
+ if err := cmd.Start(); err != nil {
+ return nil, err
+ }
+ return &Tracer{
+ pid: cmd.Process.Pid,
+ cmd: cmd,
+ }, nil
+}
+
+func (t *Tracer) SetSyscallExitHandler(handler func(*Syscall)) {
+ t.handlers.syscallExit = handler
+}
+
+func (t *Tracer) SetSyscallEnterHandler(handler func(*Syscall)) {
+ t.handlers.syscallEnter = handler
+}
+
+func (t *Tracer) SetSignalHandler(handler func(syscall.Signal)) {
+ t.handlers.signal = handler
+}
+
+func (t *Tracer) SetProcessExitHandler(handler func(int)) {
+ t.handlers.processExit = handler
+}
+
+func (t *Tracer) Start() error {
+
+ runtime.LockOSThread()
+
+ if _, err := os.FindProcess(t.pid); err != nil {
+ return fmt.Errorf("could not find process with pid %d: %w", t.pid, err)
+ }
+
+ if t.cmd == nil {
+ if err := syscall.PtraceAttach(t.pid); err == syscall.EPERM {
+ return fmt.Errorf("could not attach to process with pid %d: %w - check your permissions", t.pid, err)
+ } else if err != nil {
+ return err
+ }
+ }
+
+ status := syscall.WaitStatus(0)
+ if _, err := syscall.Wait4(t.pid, &status, 0, nil); err != nil {
+ return err
+ }
+
+ if t.cmd == nil {
+ defer func() {
+ _ = syscall.PtraceDetach(t.pid)
+ _, _ = syscall.Wait4(t.pid, &status, 0, nil)
+ }()
+ }
+
+ // deliver SIGTRAP|0x80
+ if err := syscall.PtraceSetOptions(t.pid, syscall.PTRACE_O_TRACESYSGOOD); err != nil {
+ return err
+ }
+
+ signalChan := make(chan os.Signal, 1)
+ signal.Notify(signalChan, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGPIPE, syscall.SIGQUIT)
+ go func() {
+ for sig := range signalChan {
+ interrupted := sig.(syscall.Signal)
+ fmt.Printf("((SIGNAL: %s))", interrupted) // TODO
+ _ = syscall.Kill(t.pid, syscall.SIGSTOP)
+ }
+ }()
+
+ return t.loop()
+}
+
+var errExited = fmt.Errorf("process exited")
+
+func (t *Tracer) loop() error {
+ for {
+ if err := t.waitForSyscall(); err != nil {
+ if err == errExited {
+ return nil
+ }
+ return err
+ }
+ }
+}
+
+func isStopSig(sig syscall.Signal) bool {
+ return sig == syscall.SIGSTOP || sig == syscall.SIGTSTP || sig == syscall.SIGTTIN || sig == syscall.SIGTTOU
+}
+
+func (t *Tracer) waitForSyscall() error {
+
+ // intercept syscall
+ err := syscall.PtraceSyscall(t.pid, t.lastSignal)
+ if err != nil {
+ return fmt.Errorf("could not intercept syscall: %w", err)
+ }
+
+ // wait for a syscall
+ status := syscall.WaitStatus(0)
+ _, err = syscall.Wait4(t.pid, &status, 0, nil)
+ if err != nil {
+ return fmt.Errorf("wait failed: %w", err)
+ }
+
+ if status.TrapCause() != -1 {
+ return nil
+ }
+
+ if status.Exited() {
+ if t.handlers.processExit != nil {
+ t.handlers.processExit(status.ExitStatus())
+ }
+ return errExited
+ }
+
+ if status.StopSignal() != syscall.SIGTRAP|0x80 {
+
+ // NOTE: once https://groups.google.com/g/golang-codereviews/c/t2SwaIV-hFs is merged, we can use
+ // syscall.PtraceGetSigInfo() to retrieve the siginfo_t struct and pass it to the signal handler instead
+ if t.handlers.signal != nil {
+ t.handlers.signal(status.StopSignal())
+ }
+
+ if isStopSig(status.StopSignal()) {
+ // TODO: if we received an interrupt via notify above, return an error here to break the loop
+ t.lastSignal = int(status.StopSignal())
+ } else if t.lastSignal != 0 {
+ if status.StopSignal() == syscall.SIGCONT {
+ t.lastSignal = 0
+ return nil
+ }
+ if err = syscall.PtraceSyscall(t.pid, t.lastSignal); err != nil && err != syscall.ESRCH {
+ return err
+ }
+ return nil
+ }
+ return nil
+ }
+
+ // if interrupted, stop tracing
+ if status.StopSignal().String() == "interrupt" {
+ _ = syscall.PtraceSyscall(t.pid, int(status.StopSignal()))
+ return fmt.Errorf("process interrupted")
+ }
+
+ // read registers
+ regs := &syscall.PtraceRegs{}
+ if err := syscall.PtraceGetRegs(t.pid, regs); err != nil {
+ return fmt.Errorf("failed to read registers: %w", err)
+ }
+
+ call := parseSyscall(regs)
+ call.pid = t.pid
+
+ if call.number == -1 {
+ return fmt.Errorf("expecting syscall but received -1 - did we miss a signal?")
+ }
+
+ if t.isExit && t.lastCall != nil {
+ if call.number == t.lastCall.number {
+ call.args = t.lastCall.args
+ } else {
+ return fmt.Errorf("syscall exit mismatch: %d != %d - this is likely a bug in grace due to an unprocessed signal", call.number, t.lastCall.number)
+ }
+ }
+
+ if err := call.populate(t.isExit); err != nil {
+ return fmt.Errorf("populate failed: %w", err)
+ }
+
+ if t.isExit {
+ if t.handlers.syscallExit != nil {
+ t.handlers.syscallExit(call)
+ }
+ } else if t.handlers.syscallEnter != nil {
+ t.handlers.syscallEnter(call)
+ }
+ t.lastCall = call
+ t.isExit = !t.isExit
+ return nil
+}
diff --git a/tracer/types.go b/tracer/types.go
new file mode 100644
index 0000000..dc9f8d7
--- /dev/null
+++ b/tracer/types.go
@@ -0,0 +1,29 @@
+package tracer
+
+import (
+ "fmt"
+ "sync"
+)
+
+type typeHandler func(arg *Arg, metadata ArgMetadata, raw uintptr, next uintptr, ret uintptr, pid int) error
+
+var typesRegistry = map[ArgType]typeHandler{}
+var typesRegistryMutex = sync.RWMutex{}
+
+func registerTypeHandler(t ArgType, h typeHandler) {
+ typesRegistryMutex.Lock()
+ defer typesRegistryMutex.Unlock()
+ if _, ok := typesRegistry[t]; ok {
+ panic(fmt.Sprintf("type handler for %d already registered", t))
+ }
+ typesRegistry[t] = h
+}
+
+func handleType(arg *Arg, metadata ArgMetadata, raw uintptr, next uintptr, ret uintptr, pid int) error {
+ typesRegistryMutex.RLock()
+ defer typesRegistryMutex.RUnlock()
+ if h, ok := typesRegistry[metadata.Type]; ok {
+ return h(arg, metadata, raw, next, ret, pid)
+ }
+ return fmt.Errorf("no handler registered for type %d", metadata.Type)
+}
diff --git a/tracer/types_data.go b/tracer/types_data.go
new file mode 100644
index 0000000..f4fa39c
--- /dev/null
+++ b/tracer/types_data.go
@@ -0,0 +1,65 @@
+package tracer
+
+import (
+ "fmt"
+ "syscall"
+)
+
+func init() {
+ registerTypeHandler(ArgTypeData, func(arg *Arg, metadata ArgMetadata, raw uintptr, next uintptr, ret uintptr, pid int) error {
+ switch metadata.CountFrom {
+ case CountLocationNext:
+ data, err := readSize(pid, raw, next)
+ if err != nil {
+ return err
+ }
+ arg.data = data
+ case CountLocationResult:
+ data, err := readSize(pid, raw, ret)
+ if err != nil {
+ return err
+ }
+ arg.data = data
+ case CountLocationNullTerminator:
+ str, err := readString(pid, raw)
+ if err != nil {
+ return err
+ }
+ arg.data = []byte(str)
+ default:
+ return fmt.Errorf("syscall %s has no supported count location", metadata.Name)
+ }
+ return nil
+ })
+}
+
+func readSize(pid int, addr uintptr, size uintptr) ([]byte, error) {
+ if size == 0 {
+ return nil, nil
+ }
+ data := make([]byte, size)
+ count, err := syscall.PtracePeekData(pid, addr, data)
+ if err != nil {
+ return nil, fmt.Errorf("read of 0x%x (%d) failed: %w", addr, size, err)
+ }
+ return data[:count], nil
+}
+
+func readString(pid int, addr uintptr) (string, error) {
+ var output string
+ if addr == 0 {
+ return output, nil
+ }
+ data := make([]byte, 1)
+ for {
+ if _, err := syscall.PtracePeekData(pid, addr, data); err != nil {
+ return "", fmt.Errorf("read of 0x%x failed: %w", addr, err)
+ }
+ if data[0] == 0 {
+ break
+ }
+ output += string(data)
+ addr++
+ }
+ return output, nil
+}
diff --git a/tracer/types_int.go b/tracer/types_int.go
new file mode 100644
index 0000000..dec7e7c
--- /dev/null
+++ b/tracer/types_int.go
@@ -0,0 +1,42 @@
+package tracer
+
+import (
+ "fmt"
+ "reflect"
+)
+
+func init() {
+ registerTypeHandler(ArgTypeIntArray, func(arg *Arg, metadata ArgMetadata, raw uintptr, next uintptr, ret uintptr, pid int) error {
+ var count int
+ switch metadata.CountFrom {
+ case CountLocationNext:
+ count = int(next)
+ case CountLocationResult:
+ count = int(ret)
+ case CountLocationFixed:
+ count = metadata.FixedCount
+ default:
+ return fmt.Errorf("syscall %s has no supported count location", metadata.Name)
+ }
+
+ mem, err := readSize(pid, raw, 4*uintptr(count))
+ if err != nil {
+ return err
+ }
+
+ target := make([]int32, count)
+ if err := decodeAnonymous(reflect.ValueOf(target), mem); err != nil {
+ return err
+ }
+
+ arg.array = nil
+ for i := 0; i < count; i++ {
+ arg.array = append(arg.array, Arg{
+ t: ArgTypeInt,
+ raw: uintptr(target[i]),
+ })
+ }
+
+ return nil
+ })
+}
diff --git a/tracer/types_iovec.go b/tracer/types_iovec.go
new file mode 100644
index 0000000..0c9f9de
--- /dev/null
+++ b/tracer/types_iovec.go
@@ -0,0 +1,24 @@
+package tracer
+
+import (
+ "reflect"
+ "unsafe"
+)
+
+func init() {
+ registerTypeHandler(ArgTypeIovecArray, func(arg *Arg, metadata ArgMetadata, raw uintptr, next uintptr, ret uintptr, pid int) error {
+ // read the raw C struct from the process memory
+ mem, err := readSize(pid, raw, next*unsafe.Sizeof(iovec{}))
+ if err != nil {
+ return err
+ }
+
+ iovecs := make([]iovec, next)
+ if err := decodeAnonymous(reflect.ValueOf(iovecs), mem); err != nil {
+ return err
+ }
+
+ arg.array = convertIovecs(iovecs)
+ return nil
+ })
+}
diff --git a/tracer/types_pollfd.go b/tracer/types_pollfd.go
new file mode 100644
index 0000000..20a59ca
--- /dev/null
+++ b/tracer/types_pollfd.go
@@ -0,0 +1,26 @@
+package tracer
+
+import (
+ "reflect"
+ "unsafe"
+)
+
+func init() {
+ registerTypeHandler(ArgTypePollFdArray, func(arg *Arg, metadata ArgMetadata, raw uintptr, next uintptr, ret uintptr, pid int) error {
+ if raw > 0 {
+ // read the raw C struct from the process memory
+ rawPollFds, err := readSize(pid, raw, next*unsafe.Sizeof(pollfd{}))
+ if err != nil {
+ return err
+ }
+
+ pollFds := make([]pollfd, next)
+ if err := decodeAnonymous(reflect.ValueOf(pollFds), rawPollFds); err != nil {
+ return err
+ }
+
+ arg.array = convertPollFds(pollFds)
+ }
+ return nil
+ })
+}
diff --git a/tracer/types_sigaction.go b/tracer/types_sigaction.go
new file mode 100644
index 0000000..1966ec7
--- /dev/null
+++ b/tracer/types_sigaction.go
@@ -0,0 +1,139 @@
+package tracer
+
+import (
+ "strings"
+ "unsafe"
+)
+
+/*
+#include
+
+const void* sig_dfl = SIG_DFL;
+const void* sig_ign = SIG_IGN;
+*/
+import "C"
+
+type sigAction struct {
+ Handler uintptr
+ Sigaction uintptr
+ Mask int
+ Flags int
+ Restorer uintptr
+}
+
+func init() {
+ registerTypeHandler(ArgTypeSigAction, func(arg *Arg, metadata ArgMetadata, raw uintptr, next uintptr, ret uintptr, pid int) error {
+ if raw > 0 {
+
+ raw, err := readSize(pid, raw, unsafe.Sizeof(sigAction{}))
+ if err != nil {
+ return err
+ }
+
+ var action sigAction
+ if err := decodeStruct(raw, &action); err != nil {
+ return err
+ }
+
+ arg.obj = convertSigAction(&action)
+ }
+ return nil
+ })
+}
+
+func convertSigAction(action *sigAction) *Object {
+ obj := Object{
+ Name: "sigaction",
+ }
+
+ var handlerStr string
+ switch action.Handler {
+ case uintptr(C.sig_dfl):
+ handlerStr = "SIG_DFL"
+ case uintptr(C.sig_ign):
+ handlerStr = "SIG_IGN"
+ }
+ obj.Properties = append(obj.Properties, Arg{
+ name: "handler",
+ t: ArgTypeAddress,
+ raw: action.Handler,
+ annotation: handlerStr,
+ replace: handlerStr != "",
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "sigaction",
+ t: ArgTypeAddress,
+ raw: action.Sigaction,
+ })
+ obj.Properties = append(obj.Properties, Arg{
+ name: "mask",
+ t: ArgTypeInt,
+ raw: uintptr(action.Mask),
+ })
+
+ var signActionFlags []string
+ if action.Flags&C.SA_NOCLDSTOP != 0 {
+ signActionFlags = append(signActionFlags, "SA_NOCLDSTOP")
+ }
+ if action.Flags&C.SA_NOCLDWAIT != 0 {
+ signActionFlags = append(signActionFlags, "SA_NOCLDWAIT")
+ }
+ if action.Flags&C.SA_NODEFER != 0 {
+ signActionFlags = append(signActionFlags, "SA_NODEFER")
+ }
+ if action.Flags&C.SA_ONSTACK != 0 {
+ signActionFlags = append(signActionFlags, "SA_ONSTACK")
+ }
+ if action.Flags&C.SA_RESETHAND != 0 {
+ signActionFlags = append(signActionFlags, "SA_RESETHAND")
+ }
+ if action.Flags&C.SA_RESTART != 0 {
+ signActionFlags = append(signActionFlags, "SA_RESTART")
+ }
+ if action.Flags&C.SA_RESTORER != 0 {
+ signActionFlags = append(signActionFlags, "SA_RESTORER")
+ }
+ if action.Flags&C.SA_SIGINFO != 0 {
+ signActionFlags = append(signActionFlags, "SA_SIGINFO")
+ }
+ if action.Flags&C.SA_UNSUPPORTED != 0 {
+ signActionFlags = append(signActionFlags, "SA_UNSUPPORTED")
+ }
+ if action.Flags&C.SA_EXPOSE_TAGBITS != 0 {
+ signActionFlags = append(signActionFlags, "SA_EXPOSE_TAGBITS")
+ }
+ flagStr := strings.Join(signActionFlags, "|")
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "flags",
+ t: ArgTypeInt,
+ raw: uintptr(action.Flags),
+ annotation: flagStr,
+ replace: flagStr != "",
+ })
+ obj.Properties = append(obj.Properties, Arg{
+ name: "restorer",
+ t: ArgTypeAddress,
+ raw: action.Restorer,
+ })
+
+ return &obj
+}
+
+func annotateSigProcMaskFlags(arg *Arg, pid int) {
+
+ var joins []string
+ if arg.Raw()&C.SIG_BLOCK != 0 {
+ joins = append(joins, "SIG_BLOCK")
+ }
+ if arg.Raw()&C.SIG_UNBLOCK != 0 {
+ joins = append(joins, "SIG_UNBLOCK")
+ }
+ if arg.Raw()&C.SIG_SETMASK != 0 {
+ joins = append(joins, "SIG_SETMASK")
+ }
+
+ arg.annotation = strings.Join(joins, "|")
+ arg.replace = arg.annotation != ""
+}
diff --git a/tracer/types_stat.go b/tracer/types_stat.go
new file mode 100644
index 0000000..671d293
--- /dev/null
+++ b/tracer/types_stat.go
@@ -0,0 +1,152 @@
+package tracer
+
+import (
+ "fmt"
+ "golang.org/x/sys/unix"
+ "strings"
+ "syscall"
+ "unsafe"
+)
+
+func init() {
+ registerTypeHandler(ArgTypeStat, func(arg *Arg, metadata ArgMetadata, raw uintptr, next uintptr, ret uintptr, pid int) error {
+ if raw > 0 {
+ // read the raw C struct from the process memory
+ rawStat, err := readSize(pid, raw, unsafe.Sizeof(syscall.Stat_t{}))
+ if err != nil {
+ return err
+ }
+
+ // safely squish it into a syscall.Stat_t in our own memory space
+ var stat syscall.Stat_t
+ if err := decodeStruct(rawStat, &stat); err != nil {
+ return err
+ }
+
+ // convert the stat into a nice object for output
+ arg.obj = convertStat(&stat)
+ }
+ return nil
+ })
+}
+
+func convertStat(stat *syscall.Stat_t) *Object {
+ obj := Object{
+ Name: "stat",
+ }
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "mode",
+ t: ArgTypeUnsignedInt,
+ raw: uintptr(stat.Mode),
+ annotation: permModeToString(stat.Mode),
+ replace: true,
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "dev",
+ t: ArgTypeUnsignedInt,
+ raw: uintptr(stat.Dev),
+ annotation: deviceToString(stat.Dev),
+ replace: true,
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "ino",
+ t: ArgTypeUnsignedInt,
+ raw: uintptr(stat.Ino),
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "nlink",
+ t: ArgTypeUnsignedInt,
+ raw: uintptr(stat.Nlink),
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "uid",
+ t: ArgTypeUnsignedInt,
+ raw: uintptr(stat.Uid),
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "gid",
+ t: ArgTypeUnsignedInt,
+ raw: uintptr(stat.Gid),
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "blksize",
+ t: ArgTypeInt,
+ raw: uintptr(stat.Blksize),
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "blocks",
+ t: ArgTypeInt,
+ raw: uintptr(stat.Blocks),
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "size",
+ t: ArgTypeInt,
+ raw: uintptr(stat.Size),
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "nlink",
+ t: ArgTypeUnsignedInt,
+ raw: uintptr(stat.Nlink),
+ })
+
+ obj.Properties = append(obj.Properties, Arg{
+ name: "rdev",
+ t: ArgTypeUnsignedInt,
+ raw: uintptr(stat.Rdev),
+ annotation: deviceToString(stat.Rdev),
+ replace: true,
+ })
+
+ return &obj
+}
+
+func deviceToString(dev uint64) string {
+ major := "0"
+ if m := unix.Major(dev); m != 0 {
+ major = fmt.Sprintf("0x%x", m)
+ }
+ minor := "0"
+ if m := unix.Minor(dev); m != 0 {
+ minor = fmt.Sprintf("0x%x", m)
+ }
+ return fmt.Sprintf("makedev(%s, %s)", major, minor)
+}
+
+func permModeToString(mode uint32) string {
+
+ flags := map[uint32]string{
+ unix.S_IFBLK: "S_IFBLK",
+ unix.S_IFCHR: "S_IFCHR",
+ unix.S_IFIFO: "S_IFIFO",
+ unix.S_IFLNK: "S_IFLNK",
+ unix.S_IFREG: "S_IFREG",
+ unix.S_IFDIR: "S_IFDIR",
+ unix.S_IFSOCK: "S_IFSOCK",
+ unix.S_ISUID: "S_ISUID",
+ unix.S_ISGID: "S_ISGID",
+ unix.S_ISVTX: "S_ISVTX",
+ }
+
+ var joins []string
+ for flag, name := range flags {
+ if mode&syscall.S_IFMT == flag {
+ joins = append(joins, name)
+ }
+ }
+
+ perm := fmt.Sprintf("%04o", int(mode)&0777)
+
+ joins = append(joins, perm)
+
+ return strings.Join(joins, "|")
+}