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, "|") +}