Skip to content

Commit 45b7165

Browse files
authored
Overflow in fuzzing #588 (#604)
1 parent 16036b9 commit 45b7165

File tree

6 files changed

+410
-18
lines changed

6 files changed

+410
-18
lines changed
Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.utbot.fuzzer
22

3+
import kotlin.jvm.Throws
34
import kotlin.random.Random
45

56
/**
@@ -10,18 +11,59 @@ class CartesianProduct<T>(
1011
private val random: Random? = null
1112
): Iterable<List<T>> {
1213

13-
fun asSequence(): Sequence<List<T>> = iterator().asSequence()
14+
/**
15+
* Estimated number of all combinations.
16+
*/
17+
val estimatedSize: Long
18+
get() = Combinations(*lists.map { it.size }.toIntArray()).size
1419

15-
override fun iterator(): Iterator<List<T>> {
20+
@Throws(TooManyCombinationsException::class)
21+
fun asSequence(): Sequence<List<T>> {
1622
val combinations = Combinations(*lists.map { it.size }.toIntArray())
1723
val sequence = if (random != null) {
18-
val permutation = PseudoShuffledIntProgression(combinations.size, random)
19-
(0 until combinations.size).asSequence().map { combinations[permutation[it]] }
24+
sequence {
25+
forEachChunk(Int.MAX_VALUE, combinations.size) { startIndex, combinationSize, _ ->
26+
val permutation = PseudoShuffledIntProgression(combinationSize, random)
27+
val temp = IntArray(size = lists.size)
28+
for (it in 0 until combinationSize) {
29+
yield(combinations[permutation[it] + startIndex, temp])
30+
}
31+
}
32+
}
2033
} else {
2134
combinations.asSequence()
2235
}
2336
return sequence.map { combination ->
24-
combination.mapIndexedTo(mutableListOf()) { element, value -> lists[element][value] }
25-
}.iterator()
37+
combination.mapIndexedTo(ArrayList(combination.size)) { index, value -> lists[index][value] }
38+
}
39+
}
40+
41+
override fun iterator(): Iterator<List<T>> = asSequence().iterator()
42+
43+
companion object {
44+
/**
45+
* Consumer for processing blocks of input larger block.
46+
*
47+
* If source example is sized to 12 and every block is sized to 5 then consumer should be called 3 times with these values:
48+
*
49+
* 1. start = 0, size = 5, remain = 7
50+
* 2. start = 5, size = 5, remain = 2
51+
* 3. start = 10, size = 2, remain = 0
52+
*
53+
* The sum of start, size and remain should be equal to source block size.
54+
*/
55+
internal inline fun forEachChunk(
56+
chunkSize: Int,
57+
totalSize: Long,
58+
block: (start: Long, size: Int, remain: Long) -> Unit
59+
) {
60+
val iterationsCount = totalSize / chunkSize + if (totalSize % chunkSize == 0L) 0 else 1
61+
(0L until iterationsCount).forEach { iteration ->
62+
val start = iteration * chunkSize
63+
val size = minOf(chunkSize.toLong(), totalSize - start).toInt()
64+
val remain = totalSize - size - start
65+
block(start, size, remain)
66+
}
67+
}
2668
}
2769
}

utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Combinations.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,22 @@ class Combinations(vararg elementNumbers: Int): Iterable<IntArray> {
6363
*
6464
* The total count of all possible combinations is therefore `count[0]`.
6565
*/
66-
private val count: IntArray
67-
val size: Int
66+
private val count: LongArray
67+
val size: Long
6868
get() = if (count.isEmpty()) 0 else count[0]
6969

7070
init {
7171
val badValue = elementNumbers.indexOfFirst { it <= 0 }
7272
if (badValue >= 0) {
7373
throw IllegalArgumentException("Max value must be at least 1 to build combinations, but ${elementNumbers[badValue]} is found at position $badValue (list: $elementNumbers)")
7474
}
75-
count = IntArray(elementNumbers.size) { elementNumbers[it] }
75+
count = LongArray(elementNumbers.size) { elementNumbers[it].toLong() }
7676
for (i in count.size - 2 downTo 0) {
77-
count[i] = count[i] * count[i + 1]
77+
try {
78+
count[i] = StrictMath.multiplyExact(count[i], count[i + 1])
79+
} catch (e: ArithmeticException) {
80+
throw TooManyCombinationsException("Long overflow: ${count[i]} * ${count[i + 1]}")
81+
}
7882
}
7983
}
8084

@@ -94,7 +98,7 @@ class Combinations(vararg elementNumbers: Int): Iterable<IntArray> {
9498
* }
9599
* ```
96100
*/
97-
operator fun get(value: Int, target: IntArray = IntArray(count.size)): IntArray {
101+
operator fun get(value: Long, target: IntArray = IntArray(count.size)): IntArray {
98102
if (value >= size) {
99103
throw java.lang.IllegalArgumentException("Only $size values allowed")
100104
}
@@ -104,13 +108,20 @@ class Combinations(vararg elementNumbers: Int): Iterable<IntArray> {
104108
var rem = value
105109
for (i in target.indices) {
106110
target[i] = if (i < target.size - 1) {
107-
val res = rem / count[i + 1]
111+
val res = checkBoundsAndCast(rem / count[i + 1])
108112
rem %= count[i + 1]
109113
res
110114
} else {
111-
rem
115+
checkBoundsAndCast(rem)
112116
}
113117
}
114118
return target
115119
}
116-
}
120+
121+
private fun checkBoundsAndCast(value: Long): Int {
122+
check(value >= 0 && value < Int.MAX_VALUE) { "Value is out of bounds: $value" }
123+
return value.toInt()
124+
}
125+
}
126+
127+
class TooManyCombinationsException(msg: String) : RuntimeException(msg)

utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ import org.utbot.fuzzer.providers.CollectionModelProvider
1111
import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider
1212
import org.utbot.fuzzer.providers.EnumModelProvider
1313
import org.utbot.fuzzer.providers.PrimitiveWrapperModelProvider
14+
import java.lang.IllegalArgumentException
1415
import java.util.concurrent.atomic.AtomicInteger
1516
import java.util.function.IntSupplier
1617
import kotlin.random.Random
1718

18-
private val logger = KotlinLogging.logger {}
19+
private val logger by lazy { KotlinLogging.logger {} }
1920

2021
fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvider): Sequence<List<FuzzedValue>> {
22+
if (modelProviders.isEmpty()) {
23+
throw IllegalArgumentException("At least one model provider is required")
24+
}
25+
2126
val values = List<MutableList<FuzzedValue>>(description.parameters.size) { mutableListOf() }
2227
modelProviders.forEach { fuzzingProvider ->
2328
fuzzingProvider.generate(description).forEach { (index, model) ->

utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.utbot.fuzzer.providers
22

3+
import mu.KotlinLogging
34
import org.utbot.framework.plugin.api.ClassId
45
import org.utbot.framework.plugin.api.ConstructorId
56
import org.utbot.framework.plugin.api.FieldId
@@ -20,6 +21,7 @@ import org.utbot.fuzzer.FuzzedParameter
2021
import org.utbot.fuzzer.FuzzedValue
2122
import org.utbot.fuzzer.ModelProvider
2223
import org.utbot.fuzzer.ModelProvider.Companion.yieldValue
24+
import org.utbot.fuzzer.TooManyCombinationsException
2325
import org.utbot.fuzzer.exceptIsInstance
2426
import org.utbot.fuzzer.fuzz
2527
import org.utbot.fuzzer.objectModelProviders
@@ -31,6 +33,8 @@ import java.lang.reflect.Method
3133
import java.lang.reflect.Modifier.*
3234
import java.util.function.IntSupplier
3335

36+
private val logger by lazy { KotlinLogging.logger {} }
37+
3438
/**
3539
* Creates [UtAssembleModel] for objects which have public constructors with primitives types and String as parameters.
3640
*/
@@ -170,7 +174,12 @@ class ObjectModelProvider : ModelProvider {
170174
).apply {
171175
this.packageName = this@fuzzParameters.packageName
172176
}
173-
return fuzz(fuzzedMethod, *modelProviders)
177+
return try {
178+
fuzz(fuzzedMethod, *modelProviders)
179+
} catch (t: TooManyCombinationsException) {
180+
logger.warn(t) { "Number of combination of ${parameters.size} parameters is huge. Fuzzing is skipped for $name" }
181+
emptySequence()
182+
}
174183
}
175184

176185
private fun assembleModel(id: Int, constructorId: ConstructorId, params: List<FuzzedValue>): FuzzedValue {

utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CombinationsTest.kt

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@ import org.utbot.fuzzer.CartesianProduct
44
import org.utbot.fuzzer.Combinations
55
import org.junit.jupiter.api.Assertions.assertArrayEquals
66
import org.junit.jupiter.api.Assertions.assertEquals
7+
import org.junit.jupiter.api.Assertions.assertFalse
78
import org.junit.jupiter.api.Assertions.assertThrows
9+
import org.junit.jupiter.api.Assertions.assertTrue
810
import org.junit.jupiter.api.Test
11+
import org.junit.jupiter.api.assertDoesNotThrow
12+
import org.junit.jupiter.params.ParameterizedTest
13+
import org.junit.jupiter.params.provider.ValueSource
14+
import org.utbot.fuzzer.TooManyCombinationsException
15+
import java.util.BitSet
916
import kotlin.math.pow
17+
import kotlin.random.Random
1018

1119
class CombinationsTest {
1220

@@ -55,11 +63,11 @@ class CombinationsTest {
5563
val array = intArrayOf(10, 10, 10)
5664
val combinations = Combinations(*array)
5765
combinations.forEachIndexed { i, c ->
58-
var actual = 0
66+
var actual = 0L
5967
for (pos in array.indices) {
6068
actual += c[pos] * (10.0.pow(array.size - 1.0 - pos).toInt())
6169
}
62-
assertEquals(i, actual)
70+
assertEquals(i.toLong(), actual)
6371
}
6472
}
6573

@@ -105,4 +113,161 @@ class CombinationsTest {
105113
}
106114
}
107115

116+
@ParameterizedTest(name = "testAllLongValues{arguments}")
117+
@ValueSource(ints = [1, 100, Int.MAX_VALUE])
118+
fun testAllLongValues(value: Int) {
119+
val combinations = Combinations(value, value, 2)
120+
assertEquals(2L * value * value, combinations.size)
121+
val array = combinations[combinations.size - 1]
122+
assertEquals(value - 1, array[0])
123+
assertEquals(value - 1, array[1])
124+
assertEquals(1, array[2])
125+
}
126+
127+
@Test
128+
fun testCartesianFindsAllValues() {
129+
val radix = 4
130+
val product = createIntCartesianProduct(radix, 10)
131+
val total = product.estimatedSize
132+
assertTrue(total < Int.MAX_VALUE) { "This test should generate less than Int.MAX_VALUE values but has $total" }
133+
134+
val set = BitSet((total / 64).toInt())
135+
val updateSet: (List<String>) -> Unit = {
136+
val value = it.joinToString("").toLong(radix).toInt()
137+
assertFalse(set[value])
138+
set.set(value)
139+
}
140+
val realCount = product.onEach(updateSet).count()
141+
assertEquals(total, realCount.toLong())
142+
143+
for (i in 0 until total) {
144+
assertTrue(set[i.toInt()]) { "Values is not listed for index = $i" }
145+
}
146+
for (i in total until set.size()) {
147+
assertFalse(set[i.toInt()])
148+
}
149+
}
150+
151+
/**
152+
* Creates all numbers from 0 to `radix^repeat`.
153+
*
154+
* For example:
155+
*
156+
* radix = 2, repeat = 2 -> {'0', '0'}, {'0', '1'}, {'1', '0'}, {'1', '1'}
157+
* radix = 16, repeat = 1 -> {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}
158+
*/
159+
private fun createIntCartesianProduct(radix: Int, repeat: Int) =
160+
CartesianProduct(
161+
lists = (1..repeat).map {
162+
Array(radix) { it.toString(radix) }.toList()
163+
},
164+
random = Random(0)
165+
).apply {
166+
assertEquals((1L..repeat).fold(1L) { acc, _ -> acc * radix }, estimatedSize)
167+
}
168+
169+
@Test
170+
fun testCanCreateCartesianProductWithSizeGreaterThanMaxInt() {
171+
val product = createIntCartesianProduct(5, 15)
172+
assertTrue(product.estimatedSize > Int.MAX_VALUE) { "This test should generate more than Int.MAX_VALUE values but has ${product.estimatedSize}" }
173+
assertDoesNotThrow {
174+
product.first()
175+
}
176+
}
177+
178+
@Test
179+
fun testIterationWithChunksIsCorrect() {
180+
val expected = mutableListOf(
181+
Triple(0L, 5, 7L),
182+
Triple(5L, 5, 2L),
183+
Triple(10L, 2, 0L),
184+
)
185+
CartesianProduct.forEachChunk(5, 12) { start, chunk, remain ->
186+
assertEquals(expected.removeFirst(), Triple(start, chunk, remain))
187+
}
188+
assertTrue(expected.isEmpty())
189+
}
190+
191+
@Test
192+
fun testIterationWithChunksIsCorrectWhenChunkIsIntMax() {
193+
val total = 12
194+
val expected = mutableListOf(
195+
Triple(0L, total, 0L)
196+
)
197+
CartesianProduct.forEachChunk(Int.MAX_VALUE, total.toLong()) { start, chunk, remain ->
198+
assertEquals(expected.removeFirst(), Triple(start, chunk, remain))
199+
}
200+
assertTrue(expected.isEmpty())
201+
}
202+
203+
@ParameterizedTest(name = "testIterationWithChunksIsCorrectWhenChunkIs{arguments}")
204+
@ValueSource(ints = [1, 2, 3, 4, 6, 12])
205+
fun testIterationWithChunksIsCorrectWhenChunk(chunkSize: Int) {
206+
val total = 12
207+
assertTrue(total % chunkSize == 0) { "Test requires values that are dividers of the total = $total, but it is not true for $chunkSize" }
208+
val expected = (0 until total step chunkSize).map { it.toLong() }.map {
209+
Triple(it, chunkSize, total - it - chunkSize)
210+
}.toMutableList()
211+
CartesianProduct.forEachChunk(chunkSize, total.toLong()) { start, chunk, remain ->
212+
assertEquals(expected.removeFirst(), Triple(start, chunk, remain))
213+
}
214+
assertTrue(expected.isEmpty())
215+
}
216+
217+
@ParameterizedTest(name = "testIterationsWithChunksThroughLongWithRemainingIs{arguments}")
218+
@ValueSource(longs = [1L, 200L, 307, Int.MAX_VALUE - 1L, Int.MAX_VALUE.toLong()])
219+
fun testIterationsWithChunksThroughLongTotal(remaining: Long) {
220+
val expected = mutableListOf(
221+
Triple(0L, Int.MAX_VALUE, Int.MAX_VALUE + remaining),
222+
Triple(Int.MAX_VALUE.toLong(), Int.MAX_VALUE, remaining),
223+
Triple(Int.MAX_VALUE * 2L, remaining.toInt(), 0L),
224+
)
225+
CartesianProduct.forEachChunk(Int.MAX_VALUE, Int.MAX_VALUE * 2L + remaining) { start, chunk, remain ->
226+
assertEquals(expected.removeFirst(), Triple(start, chunk, remain))
227+
}
228+
assertTrue(expected.isEmpty())
229+
}
230+
231+
@Test
232+
fun testCartesianProductDoesNotThrowsExceptionBeforeOverflow() {
233+
// We assume that a standard method has no more than 7 parameters.
234+
// In this case every parameter can accept up to 511 values without Long overflow.
235+
// CartesianProduct throws exception
236+
val values = Array(511) { it }.toList()
237+
val parameters = Array(7) { values }.toList()
238+
assertDoesNotThrow {
239+
CartesianProduct(parameters, Random(0)).asSequence()
240+
}
241+
}
242+
243+
@Test
244+
fun testCartesianProductThrowsExceptionOnOverflow() {
245+
// We assume that a standard method has no more than 7 parameters.
246+
// In this case every parameter can accept up to 511 values without Long overflow.
247+
// CartesianProduct throws exception
248+
val values = Array(512) { it }.toList()
249+
val parameters = Array(7) { values }.toList()
250+
assertThrows(TooManyCombinationsException::class.java) {
251+
CartesianProduct(parameters, Random(0)).asSequence()
252+
}
253+
}
254+
255+
@ParameterizedTest(name = "testCombinationHasValue{arguments}")
256+
@ValueSource(ints = [1, Int.MAX_VALUE])
257+
fun testCombinationHasValue(value: Int) {
258+
val combinations = Combinations(value)
259+
assertEquals(value.toLong(), combinations.size)
260+
assertEquals(value - 1, combinations[value - 1L][0])
261+
}
262+
263+
@Test
264+
fun testNoFailWhenMixedValues() {
265+
val combinations = Combinations(2, Int.MAX_VALUE)
266+
assertEquals(2 * Int.MAX_VALUE.toLong(), combinations.size)
267+
assertArrayEquals(intArrayOf(0, 0), combinations[0L])
268+
assertArrayEquals(intArrayOf(0, Int.MAX_VALUE - 1), combinations[Int.MAX_VALUE - 1L])
269+
assertArrayEquals(intArrayOf(1, 0), combinations[Int.MAX_VALUE.toLong()])
270+
assertArrayEquals(intArrayOf(1, 1), combinations[Int.MAX_VALUE + 1L])
271+
assertArrayEquals(intArrayOf(1, Int.MAX_VALUE - 1), combinations[Int.MAX_VALUE * 2L - 1])
272+
}
108273
}

0 commit comments

Comments
 (0)