├── .gitignore ├── Makefile ├── README.md ├── UNLICENSE ├── _fptracee.c ├── default.nix ├── fptracee.go ├── go.mod ├── go.sum ├── intsliceset.go ├── main.go ├── pstate.go ├── script.go ├── seccomp └── seccomp.go ├── stringsliceset.go ├── sysstate.go ├── testcmd ├── segfault.c └── testcmd.c └── trace.go /.gitignore: -------------------------------------------------------------------------------- 1 | /a 2 | /b 3 | /c 4 | /depgrapher 5 | /testcmd/segfault 6 | /testcmd/testcmd 7 | /tracee/tracee 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN_TARGETS = fptrace $(TRACEE) 2 | TEST_TARGETS = $(TESTCMD) $(SEGFAULT) 3 | TEMPS = a b c *.h */*.o 4 | 5 | TRACEE = ./_fptracee 6 | TESTCMD = testcmd/testcmd 7 | SEGFAULT = testcmd/segfault 8 | 9 | DESTDIR ?= $(shell bash -c 'GOPATH=$$(go env GOPATH); echo $${GOBIN:-$${GOPATH/:*/}/bin}') 10 | 11 | default: compile 12 | 13 | clean: 14 | rm -f $(BIN_TARGETS) $(TEST_TARGETS) $(TEMPS) 15 | 16 | compile: $(BIN_TARGETS) 17 | 18 | test: $(BIN_TARGETS) $(TEST_TARGETS) 19 | ./fptrace -tracee $(TRACEE) -d /dev/stdout $(TESTCMD) 20 | ./fptrace -tracee $(TRACEE) -d /dev/stdout -seccomp=false $(TESTCMD) 21 | ! ./fptrace -tracee $(TRACEE) -t /dev/stdout $(SEGFAULT) 22 | 23 | install: $(BIN_TARGETS) 24 | mkdir -p $(DESTDIR) 25 | cp $(BIN_TARGETS) $(DESTDIR) 26 | 27 | fptrace: *.go 28 | go build -o $@ 29 | 30 | $(TRACEE): seccomp.h 31 | 32 | seccomp.h: seccomp.go 33 | go run seccomp.go 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `fptrace` is a Linux process tracing tool that records process launches and file accesses. Results can be saved in a `deps.json` file or used to generate launcher scripts. It works like `strace` but produces machine readable output and resolves relative pathnames into absolute ones. Optionally it also records environment variables and prevents deletions. It incurs much less overhead than `strace` thanks to seccomp filtering. 4 | 5 | # `deps.json` 6 | 7 | `fptrace -d deps.json sh -c 'echo a > a; cat a | tee b; exec test -d a'` in `/tmp` makes: 8 | 9 | ```json 10 | [ 11 | { 12 | "Cmd": { 13 | "Parent": 1, "ID": 2, 14 | "Dir": "/tmp", "Path": "/bin/cat", "Args": ["cat", "a"] 15 | }, 16 | "Inputs": ["/etc/ld.so.cache", "/lib/x86_64-linux-gnu/libc.so.6", "/tmp/a"], 17 | "Outputs": ["/dev/fptrace/pipe/1"], 18 | "FDs": {"0": "/dev/stdin", "1": "/dev/fptrace/pipe/1", "2": "/dev/stderr"} 19 | }, 20 | { 21 | "Cmd": { 22 | "Parent": 1, "ID": 3, 23 | "Dir": "/tmp", "Path": "/usr/bin/tee", "Args": ["tee", "b"] 24 | }, 25 | "Inputs": ["/etc/ld.so.cache", "/lib/x86_64-linux-gnu/libc.so.6", "/dev/fptrace/pipe/1"], 26 | "Outputs": ["/tmp/b", "/dev/stdout"], 27 | "FDs": {"0": "/dev/fptrace/pipe/1", "1": "/dev/stdout", "2": "/dev/stderr"} 28 | }, 29 | { 30 | "Cmd": { 31 | "Parent": 0, "ID": 1, "Exec": 4, 32 | "Dir": "/tmp", "Path": "/bin/sh", "Args": ["sh", "-c", "echo a > a; cat a | tee b; exec false"] 33 | }, 34 | "Inputs": ["/etc/ld.so.cache", "/lib/x86_64-linux-gnu/libc.so.6"], 35 | "Outputs": ["/tmp/a"], 36 | "FDs": {"0": "/dev/stdin", "1": "/dev/stdout", "2": "/dev/stderr"} 37 | }, 38 | { 39 | "Cmd": { 40 | "Parent": 1, "ID": 4, "Exit": 1, 41 | "Dir": "/tmp", "Path": "/bin/false", "Args": ["false"] 42 | }, 43 | "Inputs": ["/etc/ld.so.cache", "/lib/x86_64-linux-gnu/libc.so.6"], 44 | "Outputs": [], 45 | "FDs": {"0": "/dev/stdin", "1": "/dev/stdout", "2": "/dev/stderr"} 46 | } 47 | ] 48 | ``` 49 | 50 | The result is a list of command executions (ordered by the time of their exit): an execution begins with an `execve` and ends with the last spawned thread or fork. 51 | 52 | - `ID` is a unique execution identifier (counting from 1) 53 | - `Parent` is the `ID` of the execution that spawned it 54 | - `Exit` is the exit code of the first process of the execution (omitted if zero, negative on death by signal) 55 | - `Exec` is the ID of next execution, if the first process has spawned it before the exit 56 | - `Dir` is the initial working directory 57 | - `Path` is an absolute path to the executable 58 | - `Args` are `execve` arguments 59 | - `FDs` are initial file descriptors 60 | 61 | `Inputs` and `Outputs` list chronologically absolute paths to files opened for reading and writing, except that files opened for writing and later opened for reading are not listed as execution `Inputs`. `/dev/fptrace/pipe/` is a fictional directory that enumerates pipes. 62 | 63 | # Launcher scripts 64 | 65 | `fptrace -s /tmp/scripts sh -c 'echo a > a; cat a | tee b'` generates `0-1-sh`, `1-2-cat`, and `1-3-tee`: 66 | 67 | - `0-1-sh` 68 | ```sh 69 | #!/bin/sh 70 | cd /tmp 71 | ${exec:-exec} sh -c 'echo a > a; cat a | tee b' "$@" 72 | ``` 73 | - `1-2-cat` 74 | ```sh 75 | #!/bin/sh 76 | cd /tmp 77 | ${exec:-exec} cat a "$@" 78 | ``` 79 | - `1-3-tee` 80 | ```sh 81 | #!/bin/sh 82 | cd /tmp 83 | ${exec:-exec} tee b "$@" 84 | ``` 85 | 86 | # Installation 87 | 88 | With go get: 89 | ```sh 90 | go get github.com/orivej/fptrace 91 | go generate github.com/orivej/fptrace 92 | ``` 93 | 94 | With [Nix](https://nixos.org/nix/): 95 | ```sh 96 | nix-env -if https://github.com/orivej/fptrace/archive/master.tar.gz 97 | ``` 98 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /_fptracee.c: -------------------------------------------------------------------------------- 1 | #include "seccomp.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | int main(int argc, char **argv) { 13 | int sep; 14 | for (sep = 1; sep < argc && strcmp(argv[sep], "--") != 0; sep++); 15 | if (sep >= argc - 1) { 16 | fputs("Arguments: [-seccomp] -- program args...\n", stderr); 17 | return 1; 18 | } 19 | bool withSeccomp = sep > 1 && strcmp(argv[sep - 1], "-seccomp") == 0; 20 | 21 | if (withSeccomp) { 22 | if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { 23 | perror("no_new_privs failed"); 24 | } 25 | if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &seccomp_program) < 0) { 26 | perror("seccomp failed"); 27 | } 28 | } 29 | if (ptrace(PTRACE_TRACEME)) { 30 | perror("ptrace failed"); 31 | } 32 | raise(SIGSTOP); 33 | execvp(argv[sep + 1], argv + sep + 1); 34 | fprintf(stderr, "execvp '%s'", argv[sep + 1]); 35 | perror(" failed"); 36 | } 37 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import { }; 2 | 3 | buildGoModule rec { 4 | name = "fptrace"; 5 | src = lib.cleanSource ./.; 6 | vendorHash = "sha256-hk2FEff/37yJVlpOcca0KgSnI+gTylVhqcYiIjzp/i8="; 7 | ldflags = [ 8 | "-X main.tracee=${placeholder "out"}/bin/_fptracee" 9 | ]; 10 | subPackages = [ "." ]; 11 | preBuild = '' 12 | mkdir -p $out/bin 13 | go run seccomp/seccomp.go 14 | cc _fptracee.c -o $out/bin/_fptracee 15 | ''; 16 | overrideModAttrs.preBuild = ""; 17 | } 18 | -------------------------------------------------------------------------------- /fptracee.go: -------------------------------------------------------------------------------- 1 | //go:generate make install clean 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/kardianos/osext" 11 | ) 12 | 13 | func lookBesideExecutable(name string) (string, error) { 14 | if strings.Contains(name, "/") { 15 | return "", fmt.Errorf("path not relative to executable: %s", name) 16 | } 17 | dir, err := osext.ExecutableFolder() 18 | if err != nil { 19 | return "", err 20 | } 21 | path := filepath.Join(dir, name) 22 | return exec.LookPath(path) 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/orivej/fptrace 2 | 3 | go 1.22.5 4 | 5 | require ( 6 | github.com/djmitche/shquote v0.0.0-20151208190417-c04fd6eb8242 7 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 8 | github.com/orivej/e v1.0.0 9 | golang.org/x/net v0.32.0 10 | golang.org/x/sys v0.28.0 11 | golang.org/x/text v0.21.0 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/djmitche/shquote v0.0.0-20151208190417-c04fd6eb8242 h1:wMSVZgHtcxgx5iwOCtV+VTaQUJAWfaiQC6V/KKkNzug= 2 | github.com/djmitche/shquote v0.0.0-20151208190417-c04fd6eb8242/go.mod h1:NUXj5Gjm8JXgwMDqJW9q3L4j+8+R/3aM3W/WZjmHmyI= 3 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= 4 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 5 | github.com/orivej/e v1.0.0 h1:OyEBMhLfN7hl5EtDNgS6TK7cd86k+IT0S4Ki9jhj9/s= 6 | github.com/orivej/e v1.0.0/go.mod h1:eOxOguJBxQH6q/o7CZvmR+fh5v1LHH1sfohtgISSSFA= 7 | golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= 8 | golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= 9 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 10 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 11 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 12 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 13 | -------------------------------------------------------------------------------- /intsliceset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type IntSliceSet struct { 4 | Slice []int 5 | Has map[int]bool 6 | } 7 | 8 | func NewIntSliceSet() IntSliceSet { 9 | return IntSliceSet{ 10 | Slice: []int{}, 11 | Has: map[int]bool{}, 12 | } 13 | } 14 | 15 | func (ss *IntSliceSet) Add(x int) { 16 | if !ss.Has[x] { 17 | ss.Slice = append(ss.Slice, x) 18 | ss.Has[x] = true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | 16 | "github.com/orivej/e" 17 | "golang.org/x/sys/unix" 18 | "golang.org/x/text/collate" 19 | "golang.org/x/text/language" 20 | ) 21 | 22 | const R = 0 23 | const W = 1 24 | 25 | var importpath = "github.com/orivej/fptrace" 26 | var tracee = "_fptracee" 27 | 28 | var wstatusText = map[int]string{ 29 | syscall.PTRACE_EVENT_FORK: "fork", 30 | syscall.PTRACE_EVENT_VFORK: "vfork", 31 | syscall.PTRACE_EVENT_VFORK_DONE: "vforke", 32 | syscall.PTRACE_EVENT_CLONE: "clone", 33 | } 34 | 35 | var ( 36 | flEnv = flag.Bool("e", false, "record environment variables") 37 | flUndelete = flag.Bool("u", false, "undelete files") 38 | ) 39 | 40 | var withSeccomp, oldSeccomp bool 41 | 42 | var vercmp = collate.New(language.English, collate.Numeric) 43 | 44 | func main() { 45 | flTrace := flag.String("t", "/dev/null", "trace output `file`") 46 | flTracee := flag.String("tracee", tracee, "tracee `command`") 47 | flDeps := flag.String("d", "", "deps output `file`") 48 | flDepsWithOutput := flag.Bool("do", false, "output deps with outputs") 49 | flDepsWithExec := StringSliceSetFlag("dn", "output deps of `command`(s)") 50 | flScripts := flag.String("s", "", "scripts output `dir`") 51 | flRm := flag.Bool("rm", false, "clean up scripts output dir") 52 | flSeccomp := flag.Bool("seccomp", true, "trace with seccomp (if kernel >= 3.5)") 53 | flKernel := flag.String("kernel", kernelRelease(), "kernel release (for seccomp)") 54 | flag.Parse() 55 | e.Output = os.Stderr 56 | withSeccomp = *flSeccomp && vercmp.CompareString(*flKernel, "3.5") >= 0 57 | oldSeccomp = vercmp.CompareString(*flKernel, "4.8") < 0 58 | 59 | args := flag.Args() 60 | runtime.LockOSThread() 61 | tracee, err := lookBesideExecutable(*flTracee) 62 | if err != nil { 63 | tracee, err = exec.LookPath(*flTracee) 64 | } 65 | if err != nil { 66 | err = fmt.Errorf("%s\ntry running 'go generate %s'", err, importpath) 67 | } 68 | e.Exit(err) 69 | pid, err := trace(tracee, args) 70 | e.Exit(err) 71 | 72 | f, err := os.Create(*flTrace) 73 | e.Exit(err) 74 | defer e.CloseOrPrint(f) 75 | os.Stdout = f 76 | 77 | sys := NewSysState() 78 | cmdFDs := map[int]map[int32]string{} 79 | records := []Record{} 80 | 81 | onExec := func(p *ProcState) { 82 | fds := map[int32]string{} 83 | for fd, inode := range p.FDs { 84 | if inode != 0 { 85 | fds[fd] = sys.FS.Path(inode) 86 | } 87 | } 88 | cmdFDs[p.CurCmd.ID] = fds 89 | 90 | } 91 | if *flScripts != "" { 92 | if *flRm { 93 | err := os.RemoveAll(*flScripts) 94 | e.Exit(err) 95 | } 96 | err := os.MkdirAll(*flScripts, os.ModePerm) 97 | e.Exit(err) 98 | onExec0 := onExec 99 | onExec = func(p *ProcState) { 100 | onExec0(p) 101 | writeScript(*flScripts, *p.CurCmd) 102 | } 103 | } 104 | 105 | needOutput := *flDepsWithOutput 106 | execs := flDepsWithExec.Has 107 | needExec := len(execs) > 0 108 | onExit := func(p *ProcState) { 109 | r := p.Record(sys) 110 | n := len(r.Outputs) 111 | if (needOutput || needExec) && 112 | (!needOutput || n == 0 || n == 1 && r.Outputs[0] == "/dev/tty") && 113 | (!needExec || !execs[r.Cmd.Path] && !execs[filepath.Base(r.Cmd.Path)]) { 114 | return 115 | } 116 | r.FDs = cmdFDs[p.CurCmd.ID] 117 | delete(cmdFDs, p.CurCmd.ID) 118 | records = append(records, r) 119 | } 120 | 121 | rc := mainLoop(sys, pid, onExec, onExit) 122 | 123 | if *flDeps != "" { 124 | f, err := os.Create(*flDeps) 125 | e.Exit(err) 126 | defer e.CloseOrPrint(f) 127 | err = json.NewEncoder(f).Encode(records) 128 | e.Exit(err) 129 | } 130 | 131 | if rc < 0 { 132 | os.Exit(128 - rc) // Signum + 128 on death by signal. 133 | } 134 | os.Exit(rc) 135 | } 136 | 137 | func mainLoop(sys *SysState, mainPID int, onExec func(*ProcState), onExit func(*ProcState)) int { 138 | var err error 139 | pstates := map[int]*ProcState{} 140 | mainRC := 0 141 | 142 | p := NewProcState() 143 | p.CurDir, err = os.Getwd() 144 | e.Exit(err) 145 | p.FDs[0] = sys.FS.Inode("/dev/stdin") 146 | p.FDs[1] = sys.FS.Inode("/dev/stdout") 147 | p.FDs[2] = sys.FS.Inode("/dev/stderr") 148 | pstates[mainPID] = p 149 | 150 | suspended := map[int]int{} 151 | terminated := map[int]bool{} 152 | running := map[int]bool{mainPID: true} 153 | pidcmds := map[int]*Cmd{} // Map main PID of each execution to its Cmd. 154 | term := func(pid int) { 155 | if !terminated[pid] { 156 | terminate(pid, pstates[pid], onExit) 157 | terminated[pid] = true 158 | delete(running, pid) 159 | } 160 | } 161 | for { 162 | pid, trapCause, ok := waitForSyscall() 163 | if !ok { 164 | if cmd := pidcmds[pid]; cmd != nil { 165 | cmd.Exit = trapCause 166 | } 167 | term(pid) 168 | delete(pidcmds, pid) 169 | if mainPID == pid { 170 | mainPID, mainRC = 0, trapCause // Preserve exit status. 171 | } 172 | if len(running) == 0 { 173 | return mainRC // Exit with the last process. 174 | } 175 | continue 176 | } 177 | 178 | // Select PID state. 179 | pstate, ok := pstates[pid] 180 | if !ok { 181 | // Keep this PID suspended until we are notified of its creation. 182 | suspended[pid] = trapCause 183 | fmt.Println(pid, "_suspend") 184 | continue 185 | } 186 | 187 | wstatusSwitch: 188 | switch trapCause { 189 | case syscall.PTRACE_EVENT_FORK, 190 | syscall.PTRACE_EVENT_VFORK, 191 | syscall.PTRACE_EVENT_VFORK_DONE, 192 | syscall.PTRACE_EVENT_CLONE: 193 | // New proc. 194 | unewpid, err := syscall.PtraceGetEventMsg(pid) 195 | e.Exit(err) 196 | newpid := int(unewpid) 197 | cloneFiles := false 198 | if trapCause == syscall.PTRACE_EVENT_CLONE { 199 | regs, ok := getRegs(pid) 200 | cloneFiles = ok && regs.Rdx&syscall.CLONE_FILES != 0 201 | } 202 | pstates[newpid] = pstate.Clone(cloneFiles) 203 | running[newpid] = true 204 | delete(terminated, newpid) 205 | fmt.Println(pid, wstatusText[trapCause], newpid) 206 | // Resume suspended. 207 | if newstatus, ok := suspended[newpid]; ok { 208 | delete(suspended, newpid) 209 | resume(pid, 0, pstate.SysEnter) 210 | fmt.Println(newpid, "_resume") 211 | pid, trapCause, pstate = newpid, newstatus, pstates[newpid] 212 | goto wstatusSwitch 213 | } 214 | case syscall.PTRACE_EVENT_EXEC: 215 | if cmd := pidcmds[pid]; cmd != nil { 216 | cmd.Exec = sys.Proc.NextID() 217 | } 218 | term(pid) 219 | uoldpid, err := syscall.PtraceGetEventMsg(pid) 220 | e.Exit(err) 221 | oldpid := int(uoldpid) 222 | if oldpid != pid && pstate.IOs.Cnt != 1 { 223 | panic("lost pstate") 224 | } 225 | pstate = pstates[oldpid] 226 | term(oldpid) 227 | delete(terminated, pid) 228 | sys.Proc.Exec(pstate) 229 | pidcmds[pid] = pstate.CurCmd 230 | onExec(pstate) 231 | pstate.SysEnter = true 232 | pstates[pid] = pstate 233 | running[pid] = true 234 | fmt.Println(oldpid, "_exec", pid) 235 | case unix.PTRACE_EVENT_SECCOMP: 236 | if pstate.SysEnter { 237 | panic("seccomp trace event during syscall") 238 | } 239 | if oldSeccomp { 240 | resume(pid, 0, true) 241 | continue 242 | } 243 | fallthrough 244 | case 0: 245 | // Toggle edge. 246 | pstate.SysEnter = !pstate.SysEnter 247 | 248 | var ok bool 249 | if pstate.SysEnter { 250 | ok = sysenter(pid, pstate, sys) 251 | } else { 252 | ok = sysexit(pid, pstate, sys) 253 | } 254 | 255 | if !ok { 256 | term(pid) 257 | fmt.Println(pid, "_vanish") 258 | continue 259 | } 260 | default: 261 | panic("unexpected trap cause") 262 | } 263 | resume(pid, 0, pstate.SysEnter) 264 | } 265 | } 266 | 267 | func terminate(pid int, pstate *ProcState, onExit func(p *ProcState)) { 268 | if pstate.IOs.Cnt == 1 && pstate.CurCmd != nil { 269 | onExit(pstate) 270 | fmt.Println(pid, "record", *pstate.CurCmd) 271 | } 272 | pstate.ResetIOs() 273 | } 274 | 275 | func sysenter(pid int, pstate *ProcState, sys *SysState) bool { 276 | regs, ok := getRegs(pid) 277 | if !ok { 278 | return false 279 | } 280 | pstate.Syscall = int(regs.Orig_rax) 281 | switch pstate.Syscall { 282 | case syscall.SYS_EXECVE: 283 | pstate.NextCmd = Cmd{ 284 | Path: pstate.Abs(readString(pid, regs.Rdi)), 285 | Args: readStrings(pid, regs.Rsi), 286 | Dir: pstate.CurDir, 287 | } 288 | if *flEnv { 289 | pstate.NextCmd.Env = readStrings(pid, regs.Rdx) 290 | } 291 | fmt.Println(pid, "execve", pstate.NextCmd) 292 | case unix.SYS_EXECVEAT: 293 | pstate.NextCmd = Cmd{ 294 | Path: absAt(int32(regs.Rdi), readString(pid, regs.Rsi), pid, pstate, sys), 295 | Args: readStrings(pid, regs.Rdx), 296 | Dir: pstate.CurDir, 297 | } 298 | if *flEnv { 299 | pstate.NextCmd.Env = readStrings(pid, regs.R10) 300 | } 301 | fmt.Println(pid, "execveat", pstate.NextCmd) 302 | case syscall.SYS_UNLINK, syscall.SYS_RMDIR: 303 | if *flUndelete { 304 | regs.Orig_rax = syscall.SYS_ACCESS 305 | regs.Rsi = syscall.F_OK 306 | err := syscall.PtraceSetRegs(pid, ®s) 307 | e.Exit(err) 308 | } 309 | case syscall.SYS_UNLINKAT: 310 | if *flUndelete { 311 | regs.Orig_rax = syscall.SYS_FACCESSAT 312 | regs.R10 = regs.Rdx 313 | regs.Rdx = syscall.F_OK 314 | err := syscall.PtraceSetRegs(pid, ®s) 315 | e.Exit(err) 316 | } 317 | } 318 | return true 319 | } 320 | 321 | func sysexit(pid int, pstate *ProcState, sys *SysState) bool { 322 | regs, ok := getRegs(pid) 323 | if !ok { 324 | return false 325 | } 326 | ret := int(regs.Rax) 327 | if ret < 0 { 328 | return true 329 | } 330 | ret32 := int32(ret) 331 | if pstate.Syscall == syscall.SYS_FCNTL { 332 | switch regs.Rsi { 333 | case syscall.F_DUPFD: 334 | pstate.Syscall = syscall.SYS_DUP 335 | case syscall.F_DUPFD_CLOEXEC: 336 | pstate.Syscall = syscall.SYS_DUP3 337 | regs.Rdx = syscall.O_CLOEXEC 338 | case syscall.F_SETFD: 339 | b := regs.Rdx&syscall.FD_CLOEXEC != 0 340 | pstate.FDCX[int32(regs.Rdi)] = b 341 | fmt.Println(pid, "fcntl/setfd", regs.Rdi, b) 342 | } 343 | } 344 | switch pstate.Syscall { 345 | case syscall.SYS_OPEN, syscall.SYS_OPENAT: 346 | call, at, name, flags := "open", int32(unix.AT_FDCWD), regs.Rdi, regs.Rsi 347 | if pstate.Syscall == syscall.SYS_OPENAT { 348 | call, at, name, flags = "openat", int32(regs.Rdi), regs.Rsi, regs.Rdx 349 | } 350 | var path string 351 | switch { 352 | default: 353 | path = absAt(at, readString(pid, name), pid, pstate, sys) 354 | case flags&unix.O_TMPFILE == unix.O_TMPFILE: // It implies O_DIRECTORY. 355 | path = fmt.Sprintf("/proc/%d/fd/%d", pid, ret32) 356 | } 357 | write := flags & (syscall.O_WRONLY | syscall.O_RDWR) 358 | if write != 0 { 359 | write = W 360 | } 361 | inode := sys.FS.Inode(path) 362 | pstate.FDs[ret32] = inode 363 | if flags&syscall.O_CLOEXEC != 0 { 364 | pstate.FDCX[ret32] = true 365 | } 366 | fmt.Println(pid, call, write, path) 367 | if pstate.IOs.Map[W].Has[inode] { 368 | break // Treat reads after writes as writes only. 369 | } 370 | if !strings.HasPrefix(path, "/dev/fptrace/pipe/") { 371 | fi, err := os.Stat(path) 372 | e.Exit(err) 373 | if fi.IsDir() { 374 | break // Do not record directories. 375 | } 376 | } 377 | pstate.IOs.Map[write].Add(inode) 378 | case syscall.SYS_CHDIR: 379 | path := pstate.Abs(readString(pid, regs.Rdi)) 380 | pstate.CurDir = path 381 | fmt.Println(pid, "chdir", path) 382 | case syscall.SYS_FCHDIR: 383 | path := sys.FS.Path(pstate.FDs[int32(regs.Rdi)]) 384 | pstate.CurDir = path 385 | fmt.Println(pid, "fchdir", path) 386 | case syscall.SYS_LINK: 387 | oldpath := pstate.Abs(readString(pid, regs.Rdi)) 388 | newpath := pstate.Abs(readString(pid, regs.Rsi)) 389 | oldnode := sys.FS.Inode(oldpath) 390 | if !pstate.IOs.Map[W].Has[oldnode] { 391 | pstate.IOs.Map[R].Add(oldnode) 392 | } 393 | pstate.IOs.Map[W].Add(sys.FS.Inode(newpath)) 394 | fmt.Println(pid, "link", oldpath, newpath) 395 | case syscall.SYS_LINKAT: 396 | oldpath := absAt(int32(regs.Rdi), readString(pid, regs.Rsi), pid, pstate, sys) 397 | newpath := absAt(int32(regs.Rdx), readString(pid, regs.R10), pid, pstate, sys) 398 | oldnode := sys.FS.Inode(oldpath) 399 | if !pstate.IOs.Map[W].Has[oldnode] { 400 | pstate.IOs.Map[R].Add(oldnode) 401 | } 402 | pstate.IOs.Map[W].Add(sys.FS.Inode(newpath)) 403 | fmt.Println(pid, "linkat", oldpath, newpath) 404 | case syscall.SYS_RENAME: 405 | oldpath := pstate.Abs(readString(pid, regs.Rdi)) 406 | newpath := pstate.Abs(readString(pid, regs.Rsi)) 407 | sys.FS.Rename(oldpath, newpath) 408 | fmt.Println(pid, "rename", oldpath, newpath) 409 | case syscall.SYS_RENAMEAT, unix.SYS_RENAMEAT2: 410 | oldpath := absAt(int32(regs.Rdi), readString(pid, regs.Rsi), pid, pstate, sys) 411 | newpath := absAt(int32(regs.Rdx), readString(pid, regs.R10), pid, pstate, sys) 412 | sys.FS.Rename(oldpath, newpath) 413 | fmt.Println(pid, "renameat", oldpath, newpath) 414 | case syscall.SYS_DUP, syscall.SYS_DUP2, syscall.SYS_DUP3: 415 | pstate.FDs[ret32] = pstate.FDs[int32(regs.Rdi)] 416 | if pstate.Syscall == syscall.SYS_DUP3 && regs.Rdx&syscall.O_CLOEXEC != 0 { 417 | pstate.FDCX[ret32] = true 418 | } 419 | fmt.Println(pid, "dup", regs.Rdi, ret32, pstate.FDCX[ret32]) 420 | case syscall.SYS_READ, syscall.SYS_PREAD64, syscall.SYS_READV, syscall.SYS_PREADV, unix.SYS_PREADV2: 421 | inode := pstate.FDs[int32(regs.Rdi)] 422 | if inode != 0 && !pstate.IOs.Map[W].Has[inode] { 423 | pstate.IOs.Map[R].Add(inode) 424 | } 425 | case syscall.SYS_WRITE, syscall.SYS_PWRITE64, syscall.SYS_WRITEV, syscall.SYS_PWRITEV, unix.SYS_PWRITEV2: 426 | inode := pstate.FDs[int32(regs.Rdi)] 427 | if inode != 0 { 428 | pstate.IOs.Map[W].Add(inode) 429 | } 430 | case syscall.SYS_CLOSE: 431 | n := int32(regs.Rdi) 432 | pstate.FDs[n] = 0 433 | delete(pstate.FDCX, n) 434 | fmt.Println(pid, "close", regs.Rdi) 435 | case syscall.SYS_PIPE: 436 | var buf [8]byte 437 | _, err := syscall.PtracePeekData(pid, uintptr(regs.Rdi), buf[:]) 438 | e.Exit(err) 439 | readfd := int32(binary.LittleEndian.Uint32(buf[:4])) 440 | writefd := int32(binary.LittleEndian.Uint32(buf[4:])) 441 | inode := sys.FS.Pipe() 442 | pstate.FDs[readfd], pstate.FDs[writefd] = inode, inode 443 | if regs.Rsi&syscall.O_CLOEXEC != 0 { 444 | pstate.FDCX[readfd], pstate.FDCX[writefd] = true, true 445 | } 446 | fmt.Println(pid, "pipe", readfd, writefd, pstate.FDCX[readfd]) 447 | } 448 | return true 449 | } 450 | 451 | func absAt(dirfd int32, path string, pid int, pstate *ProcState, sys *SysState) string { 452 | switch { 453 | case dirfd == unix.AT_FDCWD: 454 | path = pstate.Abs(path) 455 | case path == "": // AT_EMPTY_PATH 456 | path = sys.FS.Path(pstate.FDs[dirfd]) 457 | default: 458 | path = pstate.AbsAt(sys.FS.Path(pstate.FDs[dirfd]), path) 459 | } 460 | 461 | // Resolve process-relative paths. 462 | if strings.HasPrefix(path, "/dev/fd/") { 463 | path = "/proc/self/fd/" + path[len("/dev/fd/"):] 464 | } 465 | if strings.HasPrefix(path, "/proc/self/") { 466 | var fd int32 467 | if _, err := fmt.Sscanf(path, "/proc/self/fd/%d", &fd); err == nil { 468 | if inode, ok := pstate.FDs[fd]; ok { 469 | return sys.FS.Path(inode) 470 | } 471 | } 472 | return strings.Replace(path, "self", strconv.Itoa(pid), 1) 473 | } 474 | return path 475 | } 476 | 477 | func kernelRelease() string { 478 | var uname syscall.Utsname 479 | err := syscall.Uname(&uname) 480 | e.Exit(err) 481 | b := []byte{} 482 | for _, c := range uname.Release { 483 | if c == 0 { 484 | break 485 | } 486 | b = append(b, byte(c)) 487 | } 488 | return string(b) 489 | } 490 | -------------------------------------------------------------------------------- /pstate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | ) 7 | 8 | type IOs struct { 9 | Cnt int // IOs reference count 10 | 11 | Map [2]IntSliceSet // input(false)/output(true) inodes 12 | } 13 | 14 | type Cmd struct { 15 | Parent int // parent Cmd ID 16 | ID int // Cmd ID, changes only with execve 17 | Exit int `json:",omitempty"` // Exit code of the first process (or 0-signal), 18 | Exec int `json:",omitempty"` // or ID of the Cmd executed by the first process 19 | 20 | Dir string 21 | Path string 22 | Args []string 23 | Env []string `json:",omitempty"` 24 | } 25 | 26 | type ProcState struct { 27 | SysEnter bool // true on enter to syscall 28 | Syscall int // call number on exit from syscall 29 | CurDir string // working directory 30 | CurCmd *Cmd // current command 31 | NextCmd Cmd // command after return from execve 32 | FDs map[int32]int // map fds to inodes 33 | FDCX map[int32]bool // cloexec fds 34 | 35 | IOs *IOs 36 | } 37 | 38 | type Record struct { 39 | Cmd Cmd 40 | Inputs []string 41 | Outputs []string 42 | FDs map[int32]string 43 | } 44 | 45 | func NewIOs() *IOs { 46 | return &IOs{1, [2]IntSliceSet{ 47 | NewIntSliceSet(), 48 | NewIntSliceSet(), 49 | }} 50 | } 51 | 52 | func NewProcState() *ProcState { 53 | return &ProcState{ 54 | FDs: make(map[int32]int), 55 | FDCX: make(map[int32]bool), 56 | IOs: NewIOs(), 57 | } 58 | } 59 | 60 | func (ps *ProcState) ResetIOs() { 61 | ps.IOs.Cnt-- 62 | ps.IOs = NewIOs() 63 | } 64 | 65 | func (ps *ProcState) Abs(p string) string { 66 | return ps.AbsAt(ps.CurDir, p) 67 | } 68 | 69 | func (ps *ProcState) AbsAt(dir, p string) string { 70 | if !path.IsAbs(p) { 71 | if !path.IsAbs(dir) { 72 | panic(fmt.Sprintf("dir is not absolute: %q", dir)) 73 | } 74 | p = path.Join(dir, p) 75 | } 76 | return path.Clean(p) 77 | } 78 | 79 | func (ps *ProcState) Clone(cloneFiles bool) *ProcState { 80 | newps := NewProcState() 81 | newps.IOs = ps.IOs // IOs are shared until exec 82 | ps.IOs.Cnt++ 83 | newps.CurDir = ps.CurDir 84 | newps.CurCmd = ps.CurCmd 85 | if cloneFiles { 86 | newps.FDs = ps.FDs 87 | newps.FDCX = ps.FDCX 88 | } else { 89 | for n, s := range ps.FDs { 90 | newps.FDs[n] = s 91 | } 92 | for n, b := range ps.FDCX { 93 | if b { 94 | newps.FDCX[n] = b 95 | } 96 | } 97 | } 98 | return newps 99 | } 100 | 101 | func (ps *ProcState) Record(sys *SysState) Record { 102 | r := Record{Cmd: *ps.CurCmd, Inputs: []string{}, Outputs: []string{}} 103 | for output, inodes := range ps.IOs.Map { 104 | paths := &r.Inputs 105 | if output == W { 106 | paths = &r.Outputs 107 | } 108 | for _, inode := range inodes.Slice { 109 | s := sys.FS.Path(inode) 110 | *paths = append(*paths, s) 111 | } 112 | } 113 | return r 114 | } 115 | -------------------------------------------------------------------------------- /script.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path" 9 | "sort" 10 | "strings" 11 | 12 | sh "github.com/djmitche/shquote" 13 | "github.com/orivej/e" 14 | ) 15 | 16 | func writeScript(dir string, cmd Cmd) { 17 | name := fmt.Sprintf("%d-%d-%s", cmd.Parent, cmd.ID, path.Base(cmd.Path)) 18 | if len(cmd.Args) == 0 { 19 | fmt.Fprintln(os.Stderr, "fptrace: can not write script with empty cmdline:", name) 20 | return 21 | } 22 | f, err := os.OpenFile(path.Join(dir, name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) //#nosec 23 | e.Exit(err) 24 | defer e.CloseOrPrint(f) 25 | 26 | interp, exec, cmdline := "#!/bin/sh", "exec", cmd.Args 27 | if cmd.Args[0] != cmd.Path { 28 | interp = "#!/usr/bin/env bash" 29 | exec = "exec -a " + sh.Quote(cmd.Args[0]) 30 | cmdline = append([]string{cmd.Path}, cmd.Args[1:]...) 31 | } 32 | buf := bufio.NewWriter(f) 33 | fmt.Fprintln(buf, interp) 34 | if len(cmd.Env) != 0 { 35 | writeEnv(buf, cmd.Env) 36 | } 37 | fmt.Fprintf(buf, "\ncd %s\n", sh.Quote(cmd.Dir)) 38 | fmt.Fprintf(buf, "\n${exec:-%s} %s \"$@\"\n", exec, sh.QuoteList(cmdline)) 39 | err = buf.Flush() 40 | e.Exit(err) 41 | } 42 | 43 | func writeEnv(buf io.Writer, env []string) { 44 | env = append([]string{}, env...) 45 | sort.Strings(env) 46 | fmt.Fprintln(buf) 47 | for _, entry := range env { 48 | parts := strings.SplitN(entry, "=", 2) 49 | if len(parts) < 2 { 50 | fmt.Fprintf(buf, ": %s\n", sh.Quote(entry)) 51 | } 52 | k, v := parts[0], parts[1] 53 | fmt.Fprintf(buf, "export %s=%s\n", sh.Quote(k), sh.Quote(v)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /seccomp/seccomp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/orivej/e" 8 | "golang.org/x/net/bpf" 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | const ( 13 | SECCOMP_RET_TRACE = 0x7ff00000 14 | SECCOMP_RET_ALLOW = 0x7fff0000 15 | ) 16 | 17 | var syscalls = []uint32{ 18 | unix.SYS_CHDIR, 19 | unix.SYS_CLOSE, 20 | unix.SYS_DUP, 21 | unix.SYS_DUP2, 22 | unix.SYS_DUP3, 23 | unix.SYS_EXECVE, 24 | unix.SYS_EXECVEAT, 25 | unix.SYS_FCHDIR, 26 | unix.SYS_FCNTL, 27 | unix.SYS_LINK, 28 | unix.SYS_LINKAT, 29 | unix.SYS_OPEN, 30 | unix.SYS_OPENAT, 31 | unix.SYS_PIPE, 32 | unix.SYS_PREAD64, 33 | unix.SYS_PREADV, 34 | unix.SYS_PREADV2, 35 | unix.SYS_PWRITE64, 36 | unix.SYS_PWRITEV, 37 | unix.SYS_PWRITEV2, 38 | unix.SYS_READ, 39 | unix.SYS_READV, 40 | unix.SYS_RENAME, 41 | unix.SYS_RENAMEAT, 42 | unix.SYS_RENAMEAT2, 43 | unix.SYS_RMDIR, 44 | unix.SYS_UNLINK, 45 | unix.SYS_UNLINKAT, 46 | unix.SYS_WRITE, 47 | unix.SYS_WRITEV, 48 | } 49 | 50 | func main() { 51 | n := len(syscalls) 52 | p := make([]bpf.Instruction, n+3) 53 | p[0] = bpf.LoadAbsolute{Off: 0, Size: 4} 54 | p[n+1] = bpf.RetConstant{Val: SECCOMP_RET_ALLOW} 55 | p[n+2] = bpf.RetConstant{Val: SECCOMP_RET_TRACE} 56 | for i, sc := range syscalls { 57 | p[i+1] = bpf.JumpIf{Cond: bpf.JumpEqual, Val: sc, SkipTrue: uint8(n - i)} 58 | } 59 | ins, err := bpf.Assemble(p) 60 | e.Exit(err) 61 | 62 | os.Stdout, err = os.Create("seccomp.h") 63 | e.Exit(err) 64 | fmt.Println("// Code generated by ./seccomp.go. DO NOT EDIT.\n") 65 | fmt.Println("#include \n") 66 | fmt.Println("struct sock_filter seccomp_filter[] = {") 67 | for _, in := range ins { 68 | fmt.Printf("\t{%#x, %d, %d, %#x},\n", in.Op, in.Jt, in.Jf, in.K) 69 | } 70 | fmt.Println("};\n") 71 | fmt.Printf("struct sock_fprog seccomp_program = {%d, seccomp_filter};\n", len(ins)) 72 | } 73 | -------------------------------------------------------------------------------- /stringsliceset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "strings" 6 | ) 7 | 8 | type StringSliceSet struct { 9 | Slice []string 10 | Has map[string]bool 11 | } 12 | 13 | func NewStringSliceSet() StringSliceSet { 14 | return StringSliceSet{ 15 | Slice: []string{}, 16 | Has: map[string]bool{}, 17 | } 18 | } 19 | 20 | func (ss *StringSliceSet) Add(x string) { 21 | if !ss.Has[x] { 22 | ss.Slice = append(ss.Slice, x) 23 | ss.Has[x] = true 24 | } 25 | } 26 | 27 | func (ss *StringSliceSet) String() string { 28 | return strings.Join(ss.Slice, ",") 29 | } 30 | 31 | func (ss *StringSliceSet) Set(x string) error { 32 | ss.Add(x) 33 | return nil 34 | } 35 | 36 | func StringSliceSetFlag(name, usage string) *StringSliceSet { 37 | ss := NewStringSliceSet() 38 | flag.Var(&ss, name, usage) 39 | return &ss 40 | } 41 | -------------------------------------------------------------------------------- /sysstate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type SysState struct { 6 | FS *FS 7 | Proc *Proc 8 | } 9 | 10 | type FS struct { 11 | seq int 12 | pipe int 13 | inodePath map[int]string 14 | pathInode map[string]int 15 | } 16 | 17 | type Proc struct { 18 | lastID int 19 | } 20 | 21 | func NewSysState() *SysState { 22 | return &SysState{FS: NewFS(), Proc: NewProc()} 23 | } 24 | 25 | func NewFS() *FS { 26 | return &FS{ 27 | inodePath: map[int]string{}, 28 | pathInode: map[string]int{}, 29 | } 30 | } 31 | 32 | func (fs *FS) Inode(path string) int { 33 | if inode, ok := fs.pathInode[path]; ok { 34 | return inode 35 | } 36 | fs.seq++ 37 | fs.inodePath[fs.seq] = path 38 | fs.pathInode[path] = fs.seq 39 | return fs.seq 40 | } 41 | 42 | func (fs *FS) Path(inode int) string { 43 | return fs.inodePath[inode] 44 | } 45 | 46 | func (fs *FS) Pipe() int { 47 | fs.pipe++ 48 | return fs.Inode(fmt.Sprint("/dev/fptrace/pipe/", fs.pipe)) 49 | } 50 | 51 | func (fs *FS) Rename(oldpath, newpath string) { 52 | if oldpath == newpath { 53 | return 54 | } 55 | oldInode := fs.pathInode[oldpath] 56 | delete(fs.pathInode, oldpath) 57 | fs.pathInode[newpath] = oldInode 58 | fs.inodePath[oldInode] = newpath 59 | } 60 | 61 | func NewProc() *Proc { 62 | return &Proc{} 63 | } 64 | 65 | func (p *Proc) NextID() int { 66 | return p.lastID + 1 67 | } 68 | 69 | func (p *Proc) Exec(ps *ProcState) { 70 | cmd := ps.NextCmd 71 | if ps.CurCmd != nil { 72 | cmd.Parent = ps.CurCmd.ID 73 | } 74 | p.lastID++ 75 | cmd.ID = p.lastID 76 | ps.CurCmd = &cmd 77 | 78 | for n, b := range ps.FDCX { 79 | if b { 80 | delete(ps.FDs, n) 81 | } 82 | } 83 | ps.FDCX = make(map[int32]bool) 84 | } 85 | -------------------------------------------------------------------------------- /testcmd/segfault.c: -------------------------------------------------------------------------------- 1 | int main() { 2 | char *ptr = (char *)100; 3 | for (;;) { 4 | ++*ptr; 5 | ++ptr; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /testcmd/testcmd.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #ifndef SYS_execveat 12 | #define SYS_execveat 322 13 | #endif 14 | 15 | int execer(void *arg) { 16 | int fd = open("/usr/bin/env", O_PATH|O_CLOEXEC); 17 | if (fd < 0) { 18 | perror("open /usr/bin/env"); 19 | } 20 | char *argv[] = {"env", "cp", "../b", "../a"}; 21 | syscall(SYS_execveat, fd, "", argv, NULL, AT_EMPTY_PATH); 22 | perror("execveat"); 23 | } 24 | 25 | int main() { 26 | /* puts("chdir"); */ 27 | if (chdir(".")) { 28 | perror("chdir"); 29 | }; 30 | 31 | /* puts("open"); */ 32 | int dirfd = open("testcmd", O_CLOEXEC); 33 | if (dirfd < 0) { 34 | perror("open testcmd"); 35 | } 36 | 37 | int fd = openat(dirfd, "../a", O_CREAT|O_WRONLY, -1); 38 | if (fd < 0) { 39 | perror("open a"); 40 | } 41 | int fd2 = dup(fd); 42 | if (write(fd2, "a\n", 2) < 0) { 43 | perror("write"); 44 | } 45 | if (rename("a", "b") < 0) { 46 | perror("rename a b"); 47 | } 48 | if (renameat(AT_FDCWD, "b", dirfd, "../c") < 0) { 49 | perror("rename b c"); 50 | } 51 | 52 | int pipefd[2]; 53 | if (pipe(pipefd)) { 54 | perror("pipe"); 55 | } 56 | char *pipe_r_path, *pipe_w_path; 57 | if (asprintf(&pipe_r_path, "/dev/fd/%d", pipefd[0]) < 0) { 58 | perror("asprintf pipe_r"); 59 | } 60 | if (asprintf(&pipe_w_path, "/proc/self/fd/%d", pipefd[1]) < 0) { 61 | perror("asprintf pipe_w"); 62 | } 63 | int pipe_r = open(pipe_r_path, O_RDONLY); 64 | if (pipe_r < 0) { 65 | perror("open pipe_r"); 66 | } 67 | int pipe_w = open(pipe_w_path, O_WRONLY); 68 | if (pipe_w < 0) { 69 | perror("open pipe_w"); 70 | } 71 | 72 | int pid = fork(); 73 | if (pid < 0) { 74 | perror("fork"); 75 | } else if (pid == 0) { 76 | char buf; 77 | if (read(pipefd[0], &buf, 1) != 1 || read(pipe_r, &buf, 1) != 1) { 78 | perror("read pipe_r"); 79 | } 80 | 81 | execlp("cp", "cp", "c", "b", NULL); 82 | perror("child execlp"); 83 | } else { 84 | if (write(pipefd[1], "a", 1) != 1 || write(pipe_w, "b", 1) != 1) { 85 | perror("write pipe_w"); 86 | } 87 | 88 | wait(NULL); 89 | } 90 | 91 | if (fchdir(dirfd)) { 92 | perror("fchdir"); 93 | }; 94 | if (close(dirfd)) { 95 | perror("close dir"); 96 | } 97 | 98 | /* execer(NULL); */ 99 | 100 | void *stack = malloc(4096000); 101 | int flags = CLONE_SIGHAND|CLONE_FS|CLONE_VM|CLONE_FILES|CLONE_THREAD; 102 | pid = clone(execer, stack+4096000, flags, NULL); 103 | if (pid < 0) { 104 | perror("clone"); 105 | } 106 | pause(); 107 | } 108 | -------------------------------------------------------------------------------- /trace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "os" 8 | "os/exec" 9 | "syscall" 10 | 11 | "github.com/orivej/e" 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | var errTraceeExited = errors.New("tracee failed to start") 16 | 17 | func getRegs(pid int) (syscall.PtraceRegs, bool) { 18 | var regs syscall.PtraceRegs 19 | err := syscall.PtraceGetRegs(pid, ®s) 20 | if err != nil && err.(syscall.Errno) == syscall.ESRCH { 21 | return regs, false 22 | } 23 | e.Exit(err) 24 | return regs, true 25 | } 26 | 27 | func readString(pid int, addr uint64) string { 28 | var chunk [64]byte 29 | var buf []byte 30 | for { 31 | n, err := syscall.PtracePeekData(pid, uintptr(addr), chunk[:]) 32 | if err != syscall.EIO { 33 | e.Print(err) 34 | } 35 | end := bytes.IndexByte(chunk[:n], 0) 36 | if end != -1 { 37 | buf = append(buf, chunk[:end]...) 38 | return string(buf) 39 | } 40 | buf = append(buf, chunk[:n]...) 41 | addr += uint64(n) 42 | } 43 | } 44 | 45 | func readStrings(pid int, addr uint64) []string { 46 | var buf [8]byte 47 | var res []string 48 | for { 49 | n, err := syscall.PtracePeekData(pid, uintptr(addr), buf[:]) 50 | e.Exit(err) 51 | saddr := binary.LittleEndian.Uint64(buf[:]) 52 | if saddr == 0 { 53 | return res 54 | } 55 | res = append(res, readString(pid, saddr)) 56 | addr += uint64(n) 57 | } 58 | } 59 | 60 | func resume(pid, signal int, duringSyscall bool) { 61 | if duringSyscall || !withSeccomp { 62 | err := syscall.PtraceSyscall(pid, signal) 63 | e.Print(err) 64 | } else { 65 | err := syscall.PtraceCont(pid, signal) 66 | e.Print(err) 67 | } 68 | } 69 | 70 | func waitForSyscall() (pid, trapcause int, alive bool) { 71 | var wstatus syscall.WaitStatus 72 | for { 73 | pid, err := syscall.Wait4(-1, &wstatus, syscall.WALL, nil) 74 | e.Exit(err) 75 | switch { 76 | case wstatus.Exited(): // Normal exit. 77 | return pid, wstatus.ExitStatus(), false 78 | case wstatus.Signaled(): // Signal exit. 79 | return pid, -int(wstatus.Signal()), false 80 | case wstatus.StopSignal()&0x80 != 0: // Ptrace stop. 81 | return pid, 0, true 82 | case wstatus.TrapCause() > 0: // SIGTRAP stop. 83 | return pid, wstatus.TrapCause(), true 84 | default: // Another signal stop (e.g. SIGSTOP). 85 | resume(pid, int(wstatus.StopSignal()), false) 86 | } 87 | } 88 | } 89 | 90 | func trace(tracee string, argv []string) (int, error) { 91 | argv = append([]string{"--"}, argv...) 92 | if withSeccomp { 93 | argv = append([]string{"-seccomp"}, argv...) 94 | } 95 | cmd := exec.Command(tracee, argv...) //#nosec 96 | cmd.Stdin = os.Stdin 97 | cmd.Stdout = os.Stdout 98 | cmd.Stderr = os.Stderr 99 | err := cmd.Start() 100 | if err != nil { 101 | return 0, err 102 | } 103 | 104 | pid := cmd.Process.Pid 105 | var wstatus syscall.WaitStatus 106 | _, err = syscall.Wait4(pid, &wstatus, 0, nil) 107 | if err != nil { 108 | return 0, err 109 | } 110 | if wstatus.Exited() { 111 | return 0, errTraceeExited 112 | } 113 | 114 | err = syscall.PtraceSetOptions(pid, 0| 115 | unix.PTRACE_O_EXITKILL| 116 | unix.PTRACE_O_TRACESECCOMP| 117 | syscall.PTRACE_O_TRACESYSGOOD| 118 | syscall.PTRACE_O_TRACEEXEC| 119 | syscall.PTRACE_O_TRACECLONE| 120 | syscall.PTRACE_O_TRACEFORK| 121 | syscall.PTRACE_O_TRACEVFORK) 122 | if err != nil { 123 | return 0, err 124 | } 125 | 126 | resume(pid, 0, false) 127 | return pid, nil 128 | } 129 | --------------------------------------------------------------------------------