Skip to content

Commit c1d197a

Browse files
kyakdanrandall77
authored andcommitted
cmd/compile: support libFuzzer value profiling mode for integer compares
libFuzzer provides a special mode known as “value profiling” in which it tracks the bit-wise progress made by the fuzzer in satisfying tracked comparisons. Furthermore, libFuzzer uses the value of the return address in its hooks to distinguish the progress for different comparisons. The original implementation of the interception for integer comparisons in Go simply called the libFuzzer hooks from a function written in Go assembly. The libFuzzer hooks thus always see the same return address (i.e., the address of the call instruction in the assembly snippet) and thus can’t distinguish individual comparisons anymore. This drastically reduces the usefulness of value profiling. This is fixed by using an assembly trampoline that injects synthetic but valid return addresses on the stack before calling the libFuzzer hook, otherwise preserving the calling convention of the respective platform (for starters, x86_64 Windows or Unix). These fake PCs are generated deterministically based on the location of the compare instruction in the IR representation. Change-Id: Iea68057c83aea7f9dc226fba7128708e8637d07a GitHub-Last-Rev: f9184ba GitHub-Pull-Request: #51321 Reviewed-on: https://go-review.googlesource.com/c/go/+/387336 Reviewed-by: Michael Knyszek <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Keith Randall <[email protected]> Reviewed-by: Keith Randall <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 3f571d1 commit c1d197a

File tree

7 files changed

+166
-58
lines changed

7 files changed

+166
-58
lines changed

src/cmd/compile/internal/typecheck/builtin.go

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/compile/internal/typecheck/builtin/runtime.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -259,14 +259,14 @@ func asanwrite(addr, size uintptr)
259259
func checkptrAlignment(unsafe.Pointer, *byte, uintptr)
260260
func checkptrArithmetic(unsafe.Pointer, []unsafe.Pointer)
261261

262-
func libfuzzerTraceCmp1(uint8, uint8)
263-
func libfuzzerTraceCmp2(uint16, uint16)
264-
func libfuzzerTraceCmp4(uint32, uint32)
265-
func libfuzzerTraceCmp8(uint64, uint64)
266-
func libfuzzerTraceConstCmp1(uint8, uint8)
267-
func libfuzzerTraceConstCmp2(uint16, uint16)
268-
func libfuzzerTraceConstCmp4(uint32, uint32)
269-
func libfuzzerTraceConstCmp8(uint64, uint64)
262+
func libfuzzerTraceCmp1(uint8, uint8, int)
263+
func libfuzzerTraceCmp2(uint16, uint16, int)
264+
func libfuzzerTraceCmp4(uint32, uint32, int)
265+
func libfuzzerTraceCmp8(uint64, uint64, int)
266+
func libfuzzerTraceConstCmp1(uint8, uint8, int)
267+
func libfuzzerTraceConstCmp2(uint16, uint16, int)
268+
func libfuzzerTraceConstCmp4(uint32, uint32, int)
269+
func libfuzzerTraceConstCmp8(uint64, uint64, int)
270270
func libfuzzerHookStrCmp(string, string, int)
271271
func libfuzzerHookEqualFold(string, string, int)
272272

src/cmd/compile/internal/walk/compare.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func walkCompare(n *ir.BinaryExpr, init *ir.Nodes) ir.Node {
153153
default:
154154
base.Fatalf("unexpected integer size %d for %v", t.Size(), t)
155155
}
156-
init.Append(mkcall(fn, nil, init, tracecmpArg(l, paramType, init), tracecmpArg(r, paramType, init)))
156+
init.Append(mkcall(fn, nil, init, tracecmpArg(l, paramType, init), tracecmpArg(r, paramType, init), fakePC(n)))
157157
}
158158
return n
159159
case types.TARRAY:

src/internal/fuzz/trace.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ import _ "unsafe" // for go:linkname
2121
//go:linkname libfuzzerHookStrCmp runtime.libfuzzerHookStrCmp
2222
//go:linkname libfuzzerHookEqualFold runtime.libfuzzerHookEqualFold
2323

24-
func libfuzzerTraceCmp1(arg0, arg1 uint8) {}
25-
func libfuzzerTraceCmp2(arg0, arg1 uint16) {}
26-
func libfuzzerTraceCmp4(arg0, arg1 uint32) {}
27-
func libfuzzerTraceCmp8(arg0, arg1 uint64) {}
28-
29-
func libfuzzerTraceConstCmp1(arg0, arg1 uint8) {}
30-
func libfuzzerTraceConstCmp2(arg0, arg1 uint16) {}
31-
func libfuzzerTraceConstCmp4(arg0, arg1 uint32) {}
32-
func libfuzzerTraceConstCmp8(arg0, arg1 uint64) {}
24+
func libfuzzerTraceCmp1(arg0, arg1 uint8, fakePC int) {}
25+
func libfuzzerTraceCmp2(arg0, arg1 uint16, fakePC int) {}
26+
func libfuzzerTraceCmp4(arg0, arg1 uint32, fakePC int) {}
27+
func libfuzzerTraceCmp8(arg0, arg1 uint64, fakePC int) {}
28+
29+
func libfuzzerTraceConstCmp1(arg0, arg1 uint8, fakePC int) {}
30+
func libfuzzerTraceConstCmp2(arg0, arg1 uint16, fakePC int) {}
31+
func libfuzzerTraceConstCmp4(arg0, arg1 uint32, fakePC int) {}
32+
func libfuzzerTraceConstCmp8(arg0, arg1 uint64, fakePC int) {}
3333

3434
func libfuzzerHookStrCmp(arg0, arg1 string, fakePC int) {}
3535
func libfuzzerHookEqualFold(arg0, arg1 string, fakePC int) {}

src/runtime/libfuzzer.go

+28-17
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,50 @@ package runtime
99
import "unsafe"
1010

1111
func libfuzzerCallWithTwoByteBuffers(fn, start, end *byte)
12+
func libfuzzerCallTraceIntCmp(fn *byte, arg0, arg1, fakePC uintptr)
1213
func libfuzzerCall4(fn *byte, fakePC uintptr, s1, s2 unsafe.Pointer, result uintptr)
13-
func libfuzzerCall(fn *byte, arg0, arg1 uintptr)
14+
// Keep in sync with the definition of ret_sled in src/runtime/libfuzzer_amd64.s
15+
const retSledSize = 512
1416

15-
func libfuzzerTraceCmp1(arg0, arg1 uint8) {
16-
libfuzzerCall(&__sanitizer_cov_trace_cmp1, uintptr(arg0), uintptr(arg1))
17+
18+
func libfuzzerTraceCmp1(arg0, arg1 uint8, fakePC int) {
19+
fakePC = fakePC % retSledSize
20+
libfuzzerCallTraceIntCmp(&__sanitizer_cov_trace_cmp1, uintptr(arg0), uintptr(arg1), uintptr(fakePC))
1721
}
1822

19-
func libfuzzerTraceCmp2(arg0, arg1 uint16) {
20-
libfuzzerCall(&__sanitizer_cov_trace_cmp2, uintptr(arg0), uintptr(arg1))
23+
func libfuzzerTraceCmp2(arg0, arg1 uint16, fakePC int) {
24+
fakePC = fakePC % retSledSize
25+
libfuzzerCallTraceIntCmp(&__sanitizer_cov_trace_cmp2, uintptr(arg0), uintptr(arg1), uintptr(fakePC))
2126
}
2227

23-
func libfuzzerTraceCmp4(arg0, arg1 uint32) {
24-
libfuzzerCall(&__sanitizer_cov_trace_cmp4, uintptr(arg0), uintptr(arg1))
28+
func libfuzzerTraceCmp4(arg0, arg1 uint32, fakePC int) {
29+
fakePC = fakePC % retSledSize
30+
libfuzzerCallTraceIntCmp(&__sanitizer_cov_trace_cmp4, uintptr(arg0), uintptr(arg1), uintptr(fakePC))
2531
}
2632

27-
func libfuzzerTraceCmp8(arg0, arg1 uint64) {
28-
libfuzzerCall(&__sanitizer_cov_trace_cmp8, uintptr(arg0), uintptr(arg1))
33+
func libfuzzerTraceCmp8(arg0, arg1 uint64, fakePC int) {
34+
fakePC = fakePC % retSledSize
35+
libfuzzerCallTraceIntCmp(&__sanitizer_cov_trace_cmp8, uintptr(arg0), uintptr(arg1), uintptr(fakePC))
2936
}
3037

31-
func libfuzzerTraceConstCmp1(arg0, arg1 uint8) {
32-
libfuzzerCall(&__sanitizer_cov_trace_const_cmp1, uintptr(arg0), uintptr(arg1))
38+
func libfuzzerTraceConstCmp1(arg0, arg1 uint8, fakePC int) {
39+
fakePC = fakePC % retSledSize
40+
libfuzzerCallTraceIntCmp(&__sanitizer_cov_trace_const_cmp1, uintptr(arg0), uintptr(arg1), uintptr(fakePC))
3341
}
3442

35-
func libfuzzerTraceConstCmp2(arg0, arg1 uint16) {
36-
libfuzzerCall(&__sanitizer_cov_trace_const_cmp2, uintptr(arg0), uintptr(arg1))
43+
func libfuzzerTraceConstCmp2(arg0, arg1 uint16, fakePC int) {
44+
fakePC = fakePC % retSledSize
45+
libfuzzerCallTraceIntCmp(&__sanitizer_cov_trace_const_cmp2, uintptr(arg0), uintptr(arg1), uintptr(fakePC))
3746
}
3847

39-
func libfuzzerTraceConstCmp4(arg0, arg1 uint32) {
40-
libfuzzerCall(&__sanitizer_cov_trace_const_cmp4, uintptr(arg0), uintptr(arg1))
48+
func libfuzzerTraceConstCmp4(arg0, arg1 uint32, fakePC int) {
49+
fakePC = fakePC % retSledSize
50+
libfuzzerCallTraceIntCmp(&__sanitizer_cov_trace_const_cmp4, uintptr(arg0), uintptr(arg1), uintptr(fakePC))
4151
}
4252

43-
func libfuzzerTraceConstCmp8(arg0, arg1 uint64) {
44-
libfuzzerCall(&__sanitizer_cov_trace_const_cmp8, uintptr(arg0), uintptr(arg1))
53+
func libfuzzerTraceConstCmp8(arg0, arg1 uint64, fakePC int) {
54+
fakePC = fakePC % retSledSize
55+
libfuzzerCallTraceIntCmp(&__sanitizer_cov_trace_const_cmp8, uintptr(arg0), uintptr(arg1), uintptr(fakePC))
4556
}
4657

4758
var pcTables []byte

src/runtime/libfuzzer_amd64.s

+69-6
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
#ifdef GOOS_windows
1414
#define RARG0 CX
1515
#define RARG1 DX
16-
#define RARG0 R8
17-
#define RARG1 R9
16+
#define RARG2 R8
17+
#define RARG3 R9
1818
#else
1919
#define RARG0 DI
2020
#define RARG1 SI
@@ -47,12 +47,39 @@ call:
4747
MOVQ R12, SP
4848
RET
4949

50-
// void runtime·libfuzzerCallTraceInit(fn, start, end *byte)
51-
// Calls C function fn from libFuzzer and passes 2 arguments to it.
52-
TEXT runtime·libfuzzerCall(SB), NOSPLIT, $0-24
50+
// void runtime·libfuzzerCallTraceIntCmp(fn, arg0, arg1, fakePC uintptr)
51+
// Calls C function fn from libFuzzer and passes 2 arguments to it after
52+
// manipulating the return address so that libfuzzer's integer compare hooks
53+
// work
54+
// libFuzzer's compare hooks obtain the caller's address from the compiler
55+
// builtin __builtin_return_adress. Since we invoke the hooks always
56+
// from the same native function, this builtin would always return the same
57+
// value. Internally, the libFuzzer hooks call through to the always inlined
58+
// HandleCmp and thus can't be mimicked without patching libFuzzer.
59+
//
60+
// We solve this problem via an inline assembly trampoline construction that
61+
// translates a runtime argument `fake_pc` in the range [0, 512) into a call to
62+
// a hook with a fake return address whose lower 9 bits are `fake_pc` up to a
63+
// constant shift. This is achieved by pushing a return address pointing into
64+
// 512 ret instructions at offset `fake_pc` onto the stack and then jumping
65+
// directly to the address of the hook.
66+
//
67+
// Note: We only set the lowest 9 bits of the return address since only these
68+
// bits are used by the libFuzzer value profiling mode for integer compares, see
69+
// https://github.com/llvm/llvm-project/blob/704d92607d26e696daba596b72cb70effe79a872/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L390
70+
// as well as
71+
// https://github.com/llvm/llvm-project/blob/704d92607d26e696daba596b72cb70effe79a872/compiler-rt/lib/fuzzer/FuzzerValueBitMap.h#L34
72+
// ValueProfileMap.AddValue() truncates its argument to 16 bits and shifts the
73+
// PC to the left by log_2(128)=7, which means that only the lowest 16 - 7 bits
74+
// of the return address matter. String compare hooks use the lowest 12 bits,
75+
// but take the return address as an argument and thus don't require the
76+
// indirection through a trampoline.
77+
// TODO: Remove the inline assembly trampoline once a PC argument has been added to libfuzzer's int compare hooks.
78+
TEXT runtime·libfuzzerCallTraceIntCmp(SB), NOSPLIT, $0-32
5379
MOVQ fn+0(FP), AX
5480
MOVQ arg0+8(FP), RARG0
5581
MOVQ arg1+16(FP), RARG1
82+
MOVQ fakePC+24(FP), R8
5683

5784
get_tls(R12)
5885
MOVQ g(R12), R14
@@ -66,10 +93,46 @@ TEXT runtime·libfuzzerCall(SB), NOSPLIT, $0-24
6693
MOVQ (g_sched+gobuf_sp)(R10), SP
6794
call:
6895
ANDQ $~15, SP // alignment for gcc ABI
69-
CALL AX
96+
// Load the address of the end of the function and push it into the stack.
97+
// This address will be jumped to after executing the return instruction
98+
// from the return sled. There we reset the stack pointer and return.
99+
MOVQ $end_of_function<>(SB), BX
100+
PUSHQ BX
101+
// Load the starting address of the return sled into BX.
102+
MOVQ $ret_sled<>(SB), BX
103+
// Load the address of the i'th return instruction fron the return sled.
104+
// The index is given in the fakePC argument.
105+
ADDQ R8, BX
106+
PUSHQ BX
107+
// Call the original function with the fakePC return address on the stack.
108+
// Function arguments arg0 and arg1 are passed in the registers specified
109+
// by the x64 calling convention.
110+
JMP AX
111+
// This code will not be executed and is only there to statisfy assembler
112+
// check of a balanced stack.
113+
not_reachable:
114+
POPQ BX
115+
POPQ BX
116+
RET
117+
118+
TEXT end_of_function<>(SB), NOSPLIT, $0-0
70119
MOVQ R12, SP
71120
RET
72121

122+
#define REPEAT_8(a) a \
123+
a \
124+
a \
125+
a \
126+
a \
127+
a \
128+
a \
129+
a
130+
131+
#define REPEAT_512(a) REPEAT_8(REPEAT_8(REPEAT_8(a)))
132+
133+
TEXT ret_sled<>(SB), NOSPLIT, $0-0
134+
REPEAT_512(RET)
135+
73136
// void runtime·libfuzzerCallWithTwoByteBuffers(fn, start, end *byte)
74137
// Calls C function fn from libFuzzer and passes 2 arguments of type *byte to it.
75138
TEXT runtime·libfuzzerCallWithTwoByteBuffers(SB), NOSPLIT, $0-24

src/runtime/libfuzzer_arm64.s

+47-13
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,23 @@
1414
#define RARG2 R2
1515
#define RARG3 R3
1616

17-
// void runtime·libfuzzerCall4(fn, hookId int, s1, s2 unsafe.Pointer, result uintptr)
18-
// Calls C function fn from libFuzzer and passes 4 arguments to it.
19-
TEXT runtime·libfuzzerCall4(SB), NOSPLIT, $0-40
17+
#define REPEAT_2(a) a a
18+
#define REPEAT_8(a) REPEAT_2(REPEAT_2(REPEAT_2(a)))
19+
#define REPEAT_128(a) REPEAT_2(REPEAT_8(REPEAT_8(a)))
20+
21+
// void runtime·libfuzzerCallTraceIntCmp(fn, arg0, arg1, fakePC uintptr)
22+
// Calls C function fn from libFuzzer and passes 2 arguments to it after
23+
// manipulating the return address so that libfuzzer's integer compare hooks
24+
// work.
25+
// The problem statment and solution are documented in detail in libfuzzer_amd64.s.
26+
// See commentary there.
27+
TEXT runtime·libfuzzerCallTraceIntCmp(SB), NOSPLIT, $8-32
2028
MOVD fn+0(FP), R9
21-
MOVD hookId+8(FP), RARG0
22-
MOVD s1+16(FP), RARG1
23-
MOVD s2+24(FP), RARG2
24-
MOVD result+32(FP), RARG3
29+
MOVD arg0+8(FP), RARG0
30+
MOVD arg1+16(FP), RARG1
31+
MOVD fakePC+24(FP), R8
32+
// Save the original return address in a local variable
33+
MOVD R30, savedRetAddr-8(SP)
2534

2635
MOVD g_m(g), R10
2736

@@ -33,16 +42,41 @@ TEXT runtime·libfuzzerCall4(SB), NOSPLIT, $0-40
3342
MOVD (g_sched+gobuf_sp)(R11), R12
3443
MOVD R12, RSP
3544
call:
36-
BL R9
45+
// Load address of the ret sled into the default register for the return
46+
// address (offset of four instructions, which means 16 bytes).
47+
ADR $16, R30
48+
// Clear the lowest 2 bits of fakePC. All ARM64 instructions are four
49+
// bytes long, so we cannot get better return address granularity than
50+
// multiples of 4.
51+
AND $-4, R8, R8
52+
// Add the offset of the fake_pc-th ret.
53+
ADD R8, R30, R30
54+
// Call the function by jumping to it and reusing all registers except
55+
// for the modified return address register R30.
56+
JMP (R9)
57+
58+
// The ret sled for ARM64 consists of 128 br instructions jumping to the
59+
// end of the function. Each instruction is 4 bytes long. The sled thus
60+
// has the same byte length of 4 * 128 = 512 as the x86_64 sled, but
61+
// coarser granularity.
62+
#define RET_SLED \
63+
JMP end_of_function;
64+
65+
REPEAT_128(RET_SLED);
66+
67+
end_of_function:
3768
MOVD R19, RSP
69+
MOVD savedRetAddr-8(SP), R30
3870
RET
3971

40-
// func runtime·libfuzzerCall(fn, arg0, arg1 uintptr)
41-
// Calls C function fn from libFuzzer and passes 2 arguments to it.
42-
TEXT runtime·libfuzzerCall(SB), NOSPLIT, $0-24
72+
// void runtime·libfuzzerCall4(fn, hookId int, s1, s2 unsafe.Pointer, result uintptr)
73+
// Calls C function fn from libFuzzer and passes 4 arguments to it.
74+
TEXT runtime·libfuzzerCall4(SB), NOSPLIT, $0-40
4375
MOVD fn+0(FP), R9
44-
MOVD arg0+8(FP), RARG0
45-
MOVD arg1+16(FP), RARG1
76+
MOVD hookId+8(FP), RARG0
77+
MOVD s1+16(FP), RARG1
78+
MOVD s2+24(FP), RARG2
79+
MOVD result+32(FP), RARG3
4680

4781
MOVD g_m(g), R10
4882

0 commit comments

Comments
 (0)