-
Notifications
You must be signed in to change notification settings - Fork 279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cover: consider tracking pairs of coverage instrumentation points #249
Comments
As far as I understand AFLs use of XOR is an attempt to get edge coverage on top of the weak binary instrumentation. LibFuzzer uses compiler instrumentation which does instrument edges, and it does not use XOR. In go-fuzz I tried to get edge coverage by adding empty else branches and default switch cases. It should cover most of the cases. I don't remember if we add special instrumentation to short-circuited && and ||, but we could. Proper edge coverage should be simpler with compiler instrumentation. |
I constructed a simple synthetic example (at the bottom) to try to illustrate a potential difference here. I then did a quick modification of go-fuzz-build/cover.go, and saw a reasonably good speed-up on that synthetic example:
That is with 4 workers. YMMV. A (heavily) simplified version of the code inserted by the normal instrumentation I think is something like: func foo() {
if a {
_go_fuzz_dep_.CoverTab[11111]++
}
if b {
_go_fuzz_dep_.CoverTab[22222]++
}
} In contrast, the modified code instead (in some cases) inserts code roughly similar to: func foo() {
var _go_fuzz_prev_loc int
if a {
_go_fuzz_dep_.CoverTab[11111 ^ _go_fuzz_prev_loc]++
_go_fuzz_prev_loc = 11111 >> 1
}
if b {
_go_fuzz_dep_.CoverTab[22222 ^ _go_fuzz_prev_loc]++
_go_fuzz_prev_loc = 22222 >> 1
}
} That again is a heavy simplification of the actual generated code, and doesn't show the automatically generated empty Here is the synthetic example, which tries for force go-fuzz to find three interesting strings: func FuzzEdge(input []byte) int {
found := 0
list := stringSlice(input)
if len(list) < 3 {
return 0
}
if list[0] == "Z1" {
found++
}
if list[1] == "Z2" {
found++
}
if list[2] == "Z3" {
found++
}
if found == 3 {
panic("bingo")
}
return 0
}
func stringSlice(input []byte) []string {
var list []string
for len(input) > 0 {
size := int(input[0])
input = input[1:]
if size > len(input) || size == 0 {
break
}
s := string(input[:size])
input = input[size:]
list = append(list, s)
}
return list
} I suspect part of the difference in speed is due to the modifications treating getting 2 out of 3 correct as a stronger signal than the vanilla go-fuzz-build. In other words, even if Z1 and Z2 have already been seen independently, if a new input contains a Z1 and Z2 together in the proper slots in the input data for the first time, with the modifications, that updates a new location in the CoverTab (vs. without the modifications, if Z1 and Z2 have already been seen independently, then Z1 and Z2 being seen together is not a new location in CoverTab, I think). One heavy caveat is this is just a quick experiment (and probably not the right way to do it in terms where to modify the code, etc.). This initial result might be useful, or might not be. Finally, the current version of the modifications linked above have a darwin-specific problem on the tip version of Go that currently causes it to fail to build (an issue with edit: sorry, the darwin-specific problem also affects Go 1.12 as well as tip. (I originally wrote just tip seemed affected). Presumably the problem is solvable, but that is the current state. |
One additional set of quick data points. A different branch can now use two previous locations to xor (as well as there is a constant at the top of go-fuzz-build/cover.go to change it to use one previous location to xor, which is closer to the approach above in #249 (comment)). In theory, it might be that xor'ing two previous locations increases the amount of "look back" examined in terms of locations hit in a particular invocation of a function, and the CoverTab location gets progressively closer to representing the actual path taken through a function. I tested against the example below, which has five strings of interest to find before "bingo", and hence this is intentionally a bit harder than the three string example above in #249 (comment). The summary is that using two previous locations seems to hit bingo faster in this test than using one previous location, and one previous location hits bingo seemingly faster than the current go-fuzz Each experiment was run 10x times, with a max allowed time for a given fuzz run of 3 minutes prior to automatically killing the run and restarting go-fuzz for the next round (for a total of 30x runs across 90 minutes of wall clock time in total). Using two previous locations:
Using one previous locations:
Using go-fuzz
The example used is similar to the one above, but extends it to also look for "Z4" and "Z5". Hunt for 5 strings example (click to expand)func FuzzEdgeDetectionMedium5(input []byte) int {
var found int
list := stringSlice(input)
if len(list) < 5 {
return 0
}
if list[0] == "Z1" {
found++
}
if list[1] == "Z2" {
found++
}
if list[2] == "Z3" {
found++
}
if list[3] == "Z4" {
found++
}
if list[4] == "Z5" {
found++
}
if found == 5 {
panic("bingo")
}
return 0
}
func stringSlice(input []byte) []string {
var list []string
for len(input) > 0 {
size := int(input[0])
input = input[1:]
if size > len(input) || size == 0 {
break
}
s := string(input[:size])
input = input[size:]
list = append(list, s)
}
return list
} One potential drawback of increasing the lookback count might be inflating the corpus unnecessarily, but I haven't looked at that really. Also, one could consider increasing the count of previous locations tracked even further than the two above (to three or four or more), though there are likely diminishing returns at some point. For example, maybe in practice two is not materially better than one, even though two outperformed one in this simple test. |
|
@dvyukov, thanks for taking a look at this, and thanks for the comments. While this is fresh, I am going to make a quick reply now (without waiting a few days to double-check everything, though I'll try to double-check within a few days and comment back here if I find I said something wrong).
There is a global, but hopefully the global is not used within a function or method (though I could have made a mistake). There is a local declaration of At first, I was just using the local declaration of
That's not a problem for normal go-build, but it caused a compilation error for my experiment, so one of the last things I did was add in the global as a simple workaround to let things compile... but hopefully that doesn't affect the local usage within a function (as long as I didn't make a silly mistake ;-). If this was to be pursued for real, a better change might be to not instrument those types of top-level declarations that are not useful for fuzzing? But as you said, given this was just an experiment, I was just looking for a simple change to make things work.
One other aspect that makes benchmarking this particular change more challenging is that it is harder to use coverage as a metric to compare before and after the change, because the change itself alters the coverage counts.
Agreed that is a danger. Also, just to be explicit, one other danger is saturating the CoverTab sooner for a given program, though I suspect that is a smaller danger compared to possibly over inflating the corpus. (Also, I'm not 100% sure, but I think right now, go-fuzz-build might insert some updates to CoverTab with locations that are effectively redundant in some case, so there might also be an opportunity to recoup some increases in CoverTab by eliminating some currently redundant locations updating CoverTab, though I'm not suggesting anyone tackle that now). Three other related comments:
In any event, thanks again for the comments. I guess this can be considered an experiment with "potentially interesting but potentially uninteresting" results.... and as I said in the first comment here, there are likely other things in the world of Go fuzzing that are more pressing compared to this... |
Ah, interesting. I missed that. I guess this can work then. |
FWIW in syzkaller I split "coverage" and "signal" into 2 completely independent things. Coverage is still plain PCs that can be used e.g. for coverage reports, and signal is some abstract numbers, can be edges/paths/something else, generic logic does not care what exactly it is. This is useful in some respects. |
I don't know what to say. Maybe. Re CoverTab, it always was supposed to be of tunable size. It's just that I did fixed 64K initially as the simplest option and then never had time/need to improve that. E.g. we could have instrumentation that does |
This is not something I would personally suggest doing right now or in the near term, but wanted to at least record the suggestion. (Also, perhaps this is already tracked somewhere else, or maybe something like this is already being done within
go-fuzz
, in which case I will learn something ;-).To my knowledge, if we ignore other things like sonar for a moment,
go-fuzz
coverage tracks individual coverage points like so:_go_fuzz_dep_.CoverTab[5262]++
where CoverTab is defined as:
Other fuzzers such as
afl-fuzz
take a different approach. For example, from http://lcamtuf.coredump.cx/afl/technical_details.txt:It seems
go-fuzz
(or a futurego test -fuzz=. ./...
) could at least consider that approachOn the other hand,
go-fuzz
has some greater sophistication in other areas, which very well might mean this is not as useful as it might appear for other fuzzers.The text was updated successfully, but these errors were encountered: