aboutsummaryrefslogtreecommitdiff
path: root/goapps/captrace/captrace.go
blob: 1ef1ace9df9a88a14ecd0ca7d12060fd3ca484c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
// Program captrace traces processes and notices when they attempt
// kernel actions that require Effective capabilities.
//
// The reference material for developing this tool was the the book
// "Linux Observabililty with BPF" by David Calavera and Lorenzo
// Fontana.
package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"kernel.org/pub/linux/libs/security/libcap/cap"
)

var (
	bpftrace = flag.String("bpftrace", "bpftrace", "command to launch bpftrace")
	debug    = flag.Bool("debug", false, "more output")
	pid      = flag.Int("pid", -1, "PID of target process to trace (-1 = trace all)")
)

type thread struct {
	PPID, Datum int
	Value       cap.Value
	Token       string
}

// mu protects these two maps.
var mu sync.Mutex

// tids tracks which PIDs we are following.
var tids = make(map[int]int)

// cache tracks in-flight cap_capable invocations.
var cache = make(map[int]*thread)

// event adds or resolves a capability event.
func event(add bool, tid int, th *thread) {
	mu.Lock()
	defer mu.Unlock()

	if len(tids) != 0 {
		if _, ok := tids[th.PPID]; !ok {
			if *debug {
				log.Printf("dropped %d %d %v event", th.PPID, tid, *th)
			}
			return
		}
		tids[tid] = th.PPID
		tids[th.PPID] = th.PPID
	}

	if add {
		cache[tid] = th
	} else {
		if b, ok := cache[tid]; ok {
			detail := ""
			if th.Datum < 0 {
				detail = fmt.Sprintf(" (%v)", syscall.Errno(-th.Datum))
			}
			task := ""
			if th.PPID != tid {
				task = fmt.Sprintf("+{%d}", tid)
			}
			log.Printf("%-16s %d%s opt=%d %q -> %d%s", b.Token, b.PPID, task, b.Datum, b.Value, th.Datum, detail)
		}
		delete(cache, tid)
	}
}

// tailTrace tails the bpftrace command output recognizing lines of
// interest.
func tailTrace(cmd *exec.Cmd, out io.Reader) {
	launched := false
	sc := bufio.NewScanner(out)
	for sc.Scan() {
		fields := strings.Split(sc.Text(), " ")
		if len(fields) < 4 {
			continue // ignore
		}
		if !launched {
			launched = true
			mu.Unlock()
		}
		switch fields[0] {
		case "CB":
			if len(fields) < 6 {
				continue
			}
			pid, err := strconv.Atoi(fields[1])
			if err != nil {
				continue
			}
			th := &thread{
				PPID: pid,
			}
			tid, err := strconv.Atoi(fields[2])
			if err != nil {
				continue
			}
			c, err := strconv.Atoi(fields[3])
			if err != nil {
				continue
			}
			th.Value = cap.Value(c)
			aud, err := strconv.Atoi(fields[4])
			if err != nil {
				continue
			}
			th.Datum = aud
			th.Token = strings.Join(fields[5:], " ")
			event(true, tid, th)
		case "CE":
			if len(fields) < 4 {
				continue
			}
			pid, err := strconv.Atoi(fields[1])
			if err != nil {
				continue
			}
			th := &thread{
				PPID: pid,
			}
			tid, err := strconv.Atoi(fields[2])
			if err != nil {
				continue
			}
			aud, err := strconv.Atoi(fields[3])
			if err != nil {
				continue
			}
			th.Datum = aud
			event(false, tid, th)
		default:
			if *debug {
				fmt.Println("unparsable:", fields)
			}
		}
	}
	if err := sc.Err(); err != nil {
		log.Fatalf("scanning failed: %v", err)
	}
}

// tracer invokes bpftool it returns an error if the invocation fails.
func tracer() (*exec.Cmd, error) {
	cmd := exec.Command(*bpftrace, "-e", `kprobe:cap_capable {
    printf("CB %d %d %d %d %s\n", pid, tid, arg2, arg3, comm);
}
kretprobe:cap_capable {
    printf("CE %d %d %d\n", pid, tid, retval);
}`)
	out, err := cmd.StdoutPipe()
	cmd.Stderr = os.Stderr
	if err != nil {
		return nil, fmt.Errorf("unable to create stdout for %q: %v", *bpftrace, err)
	}
	mu.Lock() // Unlocked on first ouput from tracer.
	if err := cmd.Start(); err != nil {
		return nil, fmt.Errorf("failed to start %q: %v", *bpftrace, err)
	}
	go tailTrace(cmd, out)
	return cmd, nil
}

func main() {
	flag.Usage = func() {
		fmt.Fprintf(flag.CommandLine.Output(), `Usage: %s [options] [command ...]

This tool monitors cap_capable() kernel execution to summarize when
Effective Flag capabilities are checked in a running process{thread}.
The monitoring is performed indirectly using the bpftrace tool.

Each line logged has a timestamp at which the tracing program is able to
summarize the return value of the check. A return value of " -> 0" implies
the check succeeded and confirms the process{thread} does have the
specified Effective capability.

The listed "opt=" value indicates some auditing context for why the
kernel needed to check the capability was Effective.

Options:
`, os.Args[0])
		flag.PrintDefaults()
	}
	flag.Parse()

	tr, err := tracer()
	if err != nil {
		log.Fatalf("failed to start tracer: %v", err)
	}

	mu.Lock()

	if *pid != -1 {
		tids[*pid] = *pid
	} else if len(flag.Args()) != 0 {
		args := flag.Args()
		cmd := exec.Command(args[0], args[1:]...)
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		if err := cmd.Start(); err != nil {
			log.Fatalf("failed to start %v: %v", flag.Args(), err)
		}
		tids[cmd.Process.Pid] = cmd.Process.Pid

		// waiting for the trace to complete is racy, so we sleep
		// to obtain the last events then kill the tracer and wait
		// for it to exit. Defers are in reverse order.
		defer tr.Wait()
		defer tr.Process.Kill()
		defer time.Sleep(1 * time.Second)

		tr = cmd
	}

	mu.Unlock()
	tr.Wait()
}