Skip to content

Commit 04d5b16

Browse files
committed
New fuzzing platform
1 parent e4cf410 commit 04d5b16

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3994
-295
lines changed

docs/Fuzzing Platform.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Fuzzing Platform (FP) Design
2+
3+
**Problem:** fuzzing is a versatile technique for generating values to be used as method arguments. Normally,
4+
to generate values, one needs information on a method signature, or rather on the parameter types (if a fuzzer is
5+
able to "understand" them). _White-box_ approach also requires AST, and _grey-box_ approach needs coverage
6+
information. To generate values that may serve as method arguments, the fuzzer uses generators, mutators, and
7+
predefined values.
8+
9+
* _Generators_ yield concrete objects created by descriptions. The basic description for creating objects is _type_.
10+
Constants, regular expressions, and other structured object specifications (e.g. in HTML) may be also used as
11+
descriptions.
12+
13+
* _Mutators_ modify the object in accordance with some logic that usually means random changes. To get better
14+
results, mutators obtain feedback (information on coverage and the inner state of the
15+
program) during method call.
16+
17+
* _Predefined values_ work well for known problems, e.g. incorrect symbol sequences. To discover potential problems one can analyze parameter names as well as the specific constructs or method calls inside the method body.
18+
19+
General API for using fuzzer looks like this:
20+
21+
```kotlin
22+
fuzz(
23+
params = "number", "string", "object<object, number>: number, string",
24+
details: (constants, providers, etc)
25+
).accept { values: List ->
26+
val feedback = exec(values);
27+
return feedback
28+
}
29+
```
30+
31+
## Parameters
32+
33+
The general fuzzing process gets the list of parameter descriptions as input and returns the corresponding list of values. The simplest description is the specific object type, for example:
34+
35+
```kotlin
36+
[Int, Bool]
37+
```
38+
39+
In this particular case, the fuzzing process can generate the set of all the pairs having integer as the first value
40+
and `true` or `false` as the second one. If values `-3, 0, 10` are generated to be the `Int` values, the set of all the possible combinations has six items: `(-3, false), (0, false), (10, false), (-3, true), (0, true), (10, true)`. Depending on the programming language, one may use interface descriptions or annotations (type hints) instead of defining the specific type. Fuzzing platform (FP) is not able to create the concrete objects as it does not deal with the specific languages. It still can convert the descriptions to the known constructs it can work with.
41+
42+
Say, in most of the programming languages, any integer may be represented as a bit array, and fuzzer can construct and
43+
modify bit arrays. So, in general case, the boundary values for the integer are these bit arrays:
44+
45+
* [0, 0, 0, ..., 0] - null
46+
* [1, 0, 0, ..., 0] - minimum value
47+
* [0, 1, 1, ..., 1] - maximum value
48+
* [0, 0, ..., 0, 1] - plus 1
49+
* [1, 1, 1, ..., 1] - minus 1
50+
51+
One can correctly use this representation for unsigned integers as well:
52+
53+
* [0, 0, 0, ..., 0] - null (minimum value)
54+
* [1, 0, 0, ..., 0] - maximum value / 2
55+
* [0, 1, 1, ..., 1] - maximum value / 2 + 1
56+
* [0, 0, ..., 0, 1] - plus 1
57+
* [1, 1, 1, ..., 1] - maximum value
58+
59+
Thus, FP interprets the _Byte_ and _Unsigned Byte_ descriptions in different ways: in the former case, the maximum value is [0, 1, 1, 1, 1, 1, 1, 1], while in the latter case it is [1, 1, 1, 1, 1, 1, 1, 1]. FP types are described in details further.
60+
61+
## Refined parameter description
62+
63+
During the fuzzing process, some parameters get the refined description, for example:
64+
65+
```java
66+
public boolean isNaN(Number n) {
67+
if (!(n instanceof Double)) {
68+
return false;
69+
}
70+
return Double.isNaN((Double) n);
71+
}
72+
```
73+
74+
In the above example, let the parameter be `Integer`. Considering the feedback, the fuzzer suggests that nothing but `Double` might increase coverage, so the type may be downcasted to `Double`. This allows for filtering out a priori unfitting values.
75+
76+
## Statically and dynamically generated values
77+
Predefined, or _statically_ generated, values help to define the initial range of values, which could be used as method arguments. These values allow us to:
78+
79+
* check if it is possible to call the given method with at least some set of values as arguments,
80+
* gather statistics on executing the program,
81+
* refine the parameter description.
82+
83+
_Dynamic_ values are generated in two ways:
84+
85+
* internally — via mutating the existing values, successfully performed as method arguments (i.e. seeds);
86+
* externally — via obtaining feedback that can return not only the statistics on the execution (the paths explored,
87+
the time spent, etc.) but also the set of new values to be blended with the values already in use.
88+
89+
Dynamic values should have the higher priority for a sample, that's why they should be chosen either first or at least more likely than the statically generated ones. In general, the algorithm that guides the fuzzing process looks like this:
90+
91+
```python
92+
# dynamic values are stored with respect to their return priority
93+
dynamic_values = empty_priority_queue()
94+
# static values are generated beforehand
95+
static_values = generate()
96+
# "good" values
97+
seeded_values = []
98+
#
99+
filters = []
100+
101+
# the loop runs until coverage reaches 100%
102+
while app.should_fuzz(seeded_values.feedbacks):
103+
# first we choose all dynamic values
104+
# if there are no dynamic values, choose the static ones
105+
value = dynamic_values.take() ?: static_values.take_random()
106+
# if there is no value or it was filtered out (static values are generated in advance — they can be random and unfitting), try to generate new values via mutating the seeds
107+
if value is null or filters.is_filtered(value):
108+
value = mutate(seeded_values.random_value())
109+
# if there is still no value at this point, it means that there are no appropriate values at all, and the process stops
110+
if value is null: break
111+
112+
# run with the given values and obtain feedback
113+
feedback = yield_run(value)
114+
# feedback says if it is reasonable to add the current value to the set of seeds
115+
if feedback is good:
116+
seeded_values[feedback] += value
117+
# feedback may also provide fuzzer with the new values
118+
if feedback has suggested_value:
119+
dynamic_values += feedback.suggested_values() with high_priority
120+
121+
# mutate the static value thus allowing fuzzer to alternate static and dynamic values
122+
if value.is_static_generated:
123+
dynamic_values += mutate(seeded_values.random_value()) with low_priority
124+
```
125+
126+
## Helping fuzzer via code modification
127+
128+
Sometimes it is reasonable to modify the source code so that it makes applying fuzzer to it easier. This is one of possible approaches: to split the complex _if_-statement into the sequence of simpler _if_-statements. See [Circumventing Fuzzing Roadblocks with Compiler Transformations](https://lafintel.wordpress.com/2016/08/15/circumventing-fuzzing-roadblocks-with-compiler-transformations/) for details.
129+
130+
## Generators
131+
132+
There are two types of generators:
133+
134+
* yielding values of primitive data types: integers, strings, booleans
135+
* yielding values of recursive data types: objects, lists
136+
137+
Sometimes it is necessary not only to create an object but to modify it as well. We can apply fuzzing to
138+
the fuzzer-generated values that should be modified. For example, you have the `HashMap.java` class, and you need to
139+
generate
140+
three
141+
modifications for it using `put(key, value)`. For this purpose, you may request for applying the fuzzer to six
142+
parameters `(key, value, key, value, key, value)` and get the necessary modified values.
143+
144+
Primitive type generators allow for yielding
145+
146+
1. Signed integers of a given size (8, 16, 32, and 64 bits, usually)
147+
2. Unsigned integers of a given size
148+
3. Floating-point numbers with a given size of significand and exponent according to IEEE 754
149+
4. Booleans: _True_ and _False_
150+
5. Characters (in UTF-16 format)
151+
6. Strings (consisting of UTF-16 characters)
152+
153+
Fuzzer should be able to provide out-of-the-box support for these types — be able to create, modify, and process
154+
them. To work with multiple languages it is enough to specify the possible type size and to describe and create the
155+
concrete objects based on the FP-generated values.
156+
157+
The recursive types include two categories:
158+
159+
* Collections (arrays and lists)
160+
* Objects
161+
162+
Collections may be nested and have _n_ dimensions (one, two, three, or more).
163+
164+
Collections may be:
165+
166+
* of a fixed size (e.g., arrays)
167+
* of a variable size (e.g., lists and dictionaries)
168+
169+
Objects may have:
170+
171+
1. Constructors with parameters
172+
173+
2. Modifiable inner fields
174+
175+
3. Modifiable global values (the static ones)
176+
177+
4. Calls for modifying methods
178+
179+
FP should be able to create and describe such objects in the form of a tree. The semantics of actual modifications is under the responsibility of a programming language.
180+
181+
182+
## Typing
183+
184+
FP does not use the concept of _type_ for creating objects. Instead, FP introduces the _task_ concept — it
185+
encapsulates the description of a type, which should be used to create an object. Generally, this task consists of two
186+
blocks: the task for initializing values and the list of tasks for modifying the initialized value.
187+
188+
```
189+
Task = [
190+
Initialization: [T1, T2, T3, ..., TN]
191+
Modification(Initialization): [
192+
М1: [T1, T2, ..., TK],
193+
М2: [T1, T2, ..., TJ],
194+
МH: [T1, T2, ..., TI],
195+
]
196+
]
197+
```
198+
199+
Thus, we can group the tasks as follows:
200+
201+
```
202+
1. Trivial task = [
203+
Initialization: [INT|UNSIGNED.INT|FLOAT.POINT.NUMBER|BOOLEAN|CHAR|STRING]
204+
Modification(Initialization): []
205+
]
206+
207+
208+
2. Task for creating an array = [
209+
Initialization: [UNSIGNED.INT]
210+
Modification(UNSIGNED.INT) = [T] * UNSIGNED.INT
211+
]
212+
213+
or
214+
215+
2. Task for creating an array = [
216+
Initialization: [UNSIGNED.INT]
217+
Modification(UNSIGNED.INT) = [[T * UNSIGNED.INT]]
218+
]
219+
220+
where "*" means repeating the type the specified number of times
221+
222+
3. Task for creating an object = [
223+
Initialization: [Т1, Т2, ... ТN],
224+
Modification(UNSIGNED.INT) = [
225+
...
226+
]
227+
]
228+
229+
```
230+
231+
Therefore, each programming language defines how to interpret a certain type and how to infer it. This allows fuzzer
232+
to store and mutate complex objects without any additional support from the language.

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ include("utbot-framework-api")
2121
include("utbot-intellij")
2222
include("utbot-sample")
2323
include("utbot-fuzzers")
24+
include("utbot-fuzzing")
2425
include("utbot-junit-contest")
2526
include("utbot-analytics")
2627
include("utbot-analytics-torch")

0 commit comments

Comments
 (0)