-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinject.py
executable file
·309 lines (271 loc) · 8.46 KB
/
inject.py
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#!/usr/bin/python
# @lint-avoid-python-3-compatibility-imports
#
# This program generates a BPF program to inject error to specified functions
# for error path testing.
#
# Ther are mainly several parts involved in such use case:
# function1()
# |- function2()
# |- function3()
# |- inject_target()
#
# inject_target() is the function we're going to inject error into
# It can either be:
# - presets from bcc/tools/inject "kmalloc"|"bio"|"alloc_page"
# the same as inject.py in bcc.
# this tool will complete the parameter list.
#
# - custom function:
# needs function name along with its parameter list
# e.g "btrfs_check_tree_leaf(struct extent_buffer *eb):-EUCLEAN"
# parameter list is used for kprobe to do extra filter (see below), can be
# empty if not used.
# the return value is also needed for error override.
#
# function1/2/3() are optional, it's designed to only inject error
# for certain call chain.
#
# The final part is the extra filter in the injected function.
# E.g, to inject error for kmalloc() we override the return value of
# should_failslab(), but we may not want to fail allocation request
# with __GFP_NOFAIL flag. Then we can use the extra filter
# "!(gfpflags & __GFP_NOFAIL)".
#
# NOTE: This tool requires CONFIG_BPF_KPROBE_OVERRIDE
#
# Usage:
# inject.py [-h] [-I extra_global_header] \
# [-P probability] [-v] [-F extra_filter]
# [-C functions in callchain] target
#
# Examples:
# # inject.py -C "extent_writepages():submit_one_bio():" -P 0.01 \
# -F "!(gfpflags & __GFP_NOFAIL)" kmalloc
# This will cause any kmalloc() call to fail under
# extent_writepages()->submit_one_bio() at 1% rate, and only fail for kmalloc()
# call without __GFP_NOFAIL flag.
#
# # inject.py -C "csum_dirty_buffer()" -P 0.001 \
# "btrfs_check_leaf_full(struct btrfs_fs_info *fs_info, struct extent_buffer *eb):-EUCLEAN"
# This will cause random write tree checker to fail.
import getopt
import re
from bcc import BPF
from sys import argv
from sys import stderr
from sys import exit
c_src = '''
#include <uapi/linux/ptrace.h>
#include <linux/mm.h>
##EXTRA_HEADERS##
#define STACK_DEPTH ##STACK_DEPTH##
struct pid_struct {
/* Counters for parent functions hitted, don't handle recursive calls yet */
u64 stack_count;
};
BPF_HASH(m, u64, struct pid_struct);
int target_entry(struct pt_regs *ctx ##PARAMETERS##)
{
int ret;
u64 pid = bpf_get_current_pid_tgid();
struct pid_struct *p;
if (!(##EXTRA_FILTER##)) {
return 0;
}
if (STACK_DEPTH) {
p = m.lookup(&pid);
if (!p) {
return 0;
}
/* recursive can be triggered multiple times */
if (p->stack_count < STACK_DEPTH) {
return 0;
}
}
/*
* Probability should only affect the function meets all prerequisite.
* Early exit would make the probability unreliable.
*/
if (bpf_get_prandom_u32() > ##PROBABILITY##) {
return 0;
}
ret = bpf_override_return(ctx, ##RETURN_VALUE##);
return ret;
}
int top_entry(struct pt_regs *ctx)
{
u64 pid = bpf_get_current_pid_tgid();
struct pid_struct p_struct = {0};
struct pid_struct *p;
p_struct.stack_count++;
m.insert(&pid, &p_struct);
return 0;
}
int top_return(struct pt_regs *ctx)
{
u64 pid = bpf_get_current_pid_tgid();
struct pid_struct *p;
p = m.lookup(&pid);
if (!p)
return 0;
if (p->stack_count > 1)
p->stack_count--;
else
m.delete(&pid);
return 0;
}
int intermediate_entry(struct pt_regs *ctx)
{
u64 pid = bpf_get_current_pid_tgid();
struct pid_struct *p;
p = m.lookup(&pid);
if (!p)
return 0;
p->stack_count++;
return 0;
}
int intermediate_exit(struct pt_regs *ctx)
{
u64 pid = bpf_get_current_pid_tgid();
struct pid_struct *p;
p = m.lookup(&pid);
if (!p)
return 0;
if (p->stack_count)
p->stack_count--;
return 0;
}
'''
target_dict = {
"kmalloc" :
("should_failslab", "struct kmem_cache *s, gfp_t gfpflags", "-ENOMEM"),
"bio" : ("should_fail_bio", "struct bio *bio", "-EIO"),
"alloc_page" :
("should_fail_alloc_page", "gfp_t gfpflags, unsinged int order",
"true")
}
usage_string='''
Usage: %s \\
[-vh] [-I <extra_global_header>] [-P <probability>] [-F <extra_filter>] \\
[-C <callchain>] <target>
<target>:
The error injection target, needs ALLOW_ERROR_INJECTION() to declare.
Supports two formats for it:
- Short preset from bcc/tool/inject.py
"kmalloc", "bio" or "alloc_page" is supported.
- Full "<function name>(<parameters>):<return value>" format
E.g. "btrfs_check_leaf_full(struct extent_buffer *eb):-EUCLEAN"
-I <extra_global_header>:
Extra global header to be included. E.g "linux/fs.h".
Can be specified multiple times.
-P <probability>:
Specify the probability the target function should fail.
Please note this probability is calculated by:
<failure hits> / <valid target hits (passes callchain and filter check)>.
Thus if the callchain reduces the target hits to minimal,
there is no need to specify this option.
E.g. "0.01". Default value is 1.0.
-F <extra_filter>:
Extra filter condition to be checked before injecting error.
This happens before probability check.
E.g "!(gfpflags & __GFP_NOFAIL)". Default value is "true".
-C <callchain>:
Specify the full callchain to match, split by ':'.
Caller first.
E.g "btree_submit_bio_hook():btree_csum_one_bio()".
Default value is empty, thus no callchain requirement.
NOTE: This doesn't handle recursive call yet.
-v: verbose mode
show the C source file for debug.
-h: help
show this help info.
'''
def usage():
print(usage_string % (argv[0], ), file=stderr)
exit(1)
def parse_callchain(string):
return string.replace("()", '').split(":")
def parse_target(string):
result = {}
if string in target_dict:
result["target"] = target_dict[string][0]
result["parameters"] = target_dict[string][1]
result["return_value"] = target_dict[string][2]
return result
tmp = re.findall("\s*(.*)\s*\(\s*(.*)\s*\):\s*(.*)\s*", string)
if len(tmp[0]) != 3:
print(tmp)
print("bad target string: %s" % (string,), file=stderr)
usage()
result["target"] = tmp[0][0]
result["parameters"] = tmp[0][1]
result["return_value"] = tmp[0][2]
return result
def parse_include(extra_headers):
result = ""
for i in extra_headers:
result += "#include <%s>\n" % i
return result
verbose = False
extra_headers = []
probability = 1.0
callchain_list = []
extra_filter = '1'
try:
optlist, args = getopt.getopt(argv[1:], "hI:P:vF:C:")
except getopt.GetoptError as err:
print(str(err))
usage()
for o, a in optlist:
if o == "-h":
usage()
elif o == "-v":
verbose = True
elif o == "-I":
extra_headers.append(a)
elif o == "-P":
probability = float(a)
if (probability > 1.0):
print("probability can't be larger than 1.0")
usage()
elif o == "-C":
callchain_list = parse_callchain(a)
elif o == "-F":
extra_filter = a
else:
assert False, "unhanled option"
if len(args) != 1:
usage()
target = parse_target(args[0])
# Code replacement
c_src = c_src.replace("##EXTRA_HEADERS##", parse_include(extra_headers))
c_src = c_src.replace("##STACK_DEPTH##", str(len(callchain_list)))
if len(target["parameters"]):
c_src = c_src.replace("##PARAMETERS##", ", " + target["parameters"])
else:
c_src = c_src.replace("##PARAMETERS##", '')
c_src = c_src.replace("##RETURN_VALUE##", target["return_value"])
c_src = c_src.replace("##PROBABILITY##", str(int(probability * ((1 << 32) - 1))))
c_src = c_src.replace("##EXTRA_FILTER##", extra_filter)
if verbose:
print(c_src)
bpf = BPF(text=c_src)
for i, func in enumerate(callchain_list):
if i == 0:
bpf.attach_kretprobe(event=func, fn_name="top_return")
bpf.attach_kprobe(event=func, fn_name="top_entry")
else:
bpf.attach_kretprobe(event=func, fn_name="intermediate_exit")
bpf.attach_kprobe(event=func, fn_name="intermediate_entry")
bpf.attach_kprobe(event=target["target"], fn_name="target_entry")
print("Probe all attached")
while True:
try:
bpf.perf_buffer_poll()
except:
for func in callchain_list:
bpf.detach_kprobe(func)
bpf.detach_kretprobe(func)
bpf.detach_kprobe(target["target"])
exit(0)