From 72d33f190ad3ab331d63cf4f1cd5736466e4beca Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Tue, 15 Apr 2025 00:57:23 +0200 Subject: [PATCH 1/2] Fix buffer resizing overflow and introduce safe power-of-two capacity growth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Previously, calculating next capacity for large values like 1_073_741_824 caused overflow and returned Integer.MIN_VALUE. - Rewrote `ensureCapacity` to use `Long` for arithmetic to guard against exceeding Int.MAX_VALUE. - Introduced `nextPowerOfTwoCapacity(minCapacity: Int)` to safely calculate the next power-of-two ≥ minCapacity. - Capped capacity at Int.MAX_VALUE to prevent buffer allocation errors. - Added comprehensive unit tests covering negative, small, exact, and large input values to validate correct capacity growth behavior. --- .../serialization/cbor/internal/Streams.kt | 37 ++++++++++-- .../cbor/internal/StreamsTest.kt | 57 +++++++++++++++++++ 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt index fdbfca674a..21e152ac8e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt @@ -43,12 +43,18 @@ internal class ByteArrayOutput { private var position: Int = 0 private fun ensureCapacity(elementsToAppend: Int) { - if (position + elementsToAppend <= array.size) { + val requiredCapacityLong = position.toLong() + elementsToAppend.toLong() + if (requiredCapacityLong > Int.MAX_VALUE) { + throw IllegalArgumentException("Required capacity exceeds maximum array size (Int.MAX_VALUE).") + } + + val requiredCapacity = requiredCapacityLong.toInt() + if (requiredCapacity <= array.size) { return } - val newArray = ByteArray((position + elementsToAppend).takeHighestOneBit() shl 1) - array.copyInto(newArray) - array = newArray + + val newCapacity = nextPowerOfTwoCapacity(requiredCapacity) + array = array.copyOf(newCapacity) } public fun toByteArray(): ByteArray { @@ -56,6 +62,29 @@ internal class ByteArrayOutput { array.copyInto(newArray, startIndex = 0, endIndex = this.position) return newArray } + + /** + * Calculates the next power-of-two capacity based on the required minimum size. + * + * This function ensures the returned value is at least as large as `minCapacity`, + * and is always a power of two, unless `minCapacity` is less than or equal to zero, + * in which case it returns 0. If the calculated power of two exceeds `Integer.MAX_VALUE`, + * it returns `Integer.MAX_VALUE`. + * + * It's useful for resizing arrays with exponential growth. + * + * @param minCapacity The minimum required capacity. + * @return A capacity value that is a power of two and ≥ minCapacity, or 0 if `minCapacity` is ≤ 0. + */ + fun nextPowerOfTwoCapacity(minCapacity: Int): Int { + if (minCapacity <= 0) return 0 + + val highestOneBit = minCapacity.takeHighestOneBit() + val maxHighestOneBit = Integer.MAX_VALUE.takeHighestOneBit() + + // Check if shifting would exceed the maximum allowed value + return if (highestOneBit < maxHighestOneBit) highestOneBit shl 1 else Integer.MAX_VALUE + } fun copyFrom(src: ByteArrayOutput) { write(src.array, count = src.position) diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt new file mode 100644 index 0000000000..958728cf76 --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt @@ -0,0 +1,57 @@ +package kotlinx.serialization.cbor.internal + +import kotlinx.serialization.* +import kotlin.test.* + +class StreamsTest { + + @Test + fun powerOfTwoCapacity_negativeValue() { + assertEquals(0, nextPowerOfTwoCapacity(-1)) + assertEquals(0, nextPowerOfTwoCapacity(-17)) + } + + @Test + fun powerOfTwoCapacity_zeroValue() { + assertEquals(0, nextPowerOfTwoCapacity(0)) + } + + @Test + fun powerOfTwoCapacity_exactPowerOfTwo() { + assertEquals(16, nextPowerOfTwoCapacity(8)) + assertEquals(32, nextPowerOfTwoCapacity(16)) + assertEquals(64, nextPowerOfTwoCapacity(32)) + } + + @Test + fun powerOfTwoCapacity_nonPowerOfTwo() { + assertEquals(16, nextPowerOfTwoCapacity(9)) + assertEquals(64, nextPowerOfTwoCapacity(33)) + assertEquals(128, nextPowerOfTwoCapacity(65)) + } + + @Test + fun powerOfTwoCapacity_smallValues() { + assertEquals(2, nextPowerOfTwoCapacity(1)) + assertEquals(4, nextPowerOfTwoCapacity(2)) + assertEquals(4, nextPowerOfTwoCapacity(3)) + } + + @Test + fun powerOfTwoCapacity_boundaryValues() { + assertEquals(0, nextPowerOfTwoCapacity(0)) + assertEquals(2, nextPowerOfTwoCapacity(1)) + assertEquals(4, nextPowerOfTwoCapacity(3)) + assertEquals(8, nextPowerOfTwoCapacity(5)) + } + + @Test + fun powerOfTwoCapacity_largeValues() { + assertEquals(1073741824, nextPowerOfTwoCapacity(536870912)) + assertEquals(1073741824, nextPowerOfTwoCapacity(1073741823)) + assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(1073741824)) + assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(1073741825)) + assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(Integer.MAX_VALUE-1)) + assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(Integer.MAX_VALUE)) + } +} From 8437a47dd1b2fd682dad910c8deba75493c1d76c Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Tue, 15 Apr 2025 15:44:47 +0200 Subject: [PATCH 2/2] Refactor: move nextPowerOfTwoCapacity to companion object in ByteArrayOutput --- .../serialization/cbor/internal/Streams.kt | 48 ++++++++++--------- .../cbor/internal/StreamsTest.kt | 44 ++++++++--------- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt index 21e152ac8e..848e58ad0e 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt @@ -62,29 +62,6 @@ internal class ByteArrayOutput { array.copyInto(newArray, startIndex = 0, endIndex = this.position) return newArray } - - /** - * Calculates the next power-of-two capacity based on the required minimum size. - * - * This function ensures the returned value is at least as large as `minCapacity`, - * and is always a power of two, unless `minCapacity` is less than or equal to zero, - * in which case it returns 0. If the calculated power of two exceeds `Integer.MAX_VALUE`, - * it returns `Integer.MAX_VALUE`. - * - * It's useful for resizing arrays with exponential growth. - * - * @param minCapacity The minimum required capacity. - * @return A capacity value that is a power of two and ≥ minCapacity, or 0 if `minCapacity` is ≤ 0. - */ - fun nextPowerOfTwoCapacity(minCapacity: Int): Int { - if (minCapacity <= 0) return 0 - - val highestOneBit = minCapacity.takeHighestOneBit() - val maxHighestOneBit = Integer.MAX_VALUE.takeHighestOneBit() - - // Check if shifting would exceed the maximum allowed value - return if (highestOneBit < maxHighestOneBit) highestOneBit shl 1 else Integer.MAX_VALUE - } fun copyFrom(src: ByteArrayOutput) { write(src.array, count = src.position) @@ -115,4 +92,29 @@ internal class ByteArrayOutput { ensureCapacity(1) array[position++] = byteValue.toByte() } + + companion object { + /** + * Calculates the next power-of-two capacity based on the required minimum size. + * + * This function ensures the returned value is at least as large as `minCapacity`, + * and is always a power of two, unless `minCapacity` is less than or equal to zero, + * in which case it returns 0. If the calculated power of two exceeds `Integer.MAX_VALUE`, + * it returns `Integer.MAX_VALUE`. + * + * It's useful for resizing arrays with exponential growth. + * + * @param minCapacity The minimum required capacity. + * @return A capacity value that is a power of two and ≥ minCapacity, or 0 if `minCapacity` is ≤ 0. + */ + fun nextPowerOfTwoCapacity(minCapacity: Int): Int { + if (minCapacity <= 0) return 0 + + val highestOneBit = minCapacity.takeHighestOneBit() + val maxHighestOneBit = Integer.MAX_VALUE.takeHighestOneBit() + + // Check if shifting would exceed the maximum allowed value + return if (highestOneBit < maxHighestOneBit) highestOneBit shl 1 else Integer.MAX_VALUE + } + } } diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt index 958728cf76..c13f09db7d 100644 --- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/internal/StreamsTest.kt @@ -7,51 +7,51 @@ class StreamsTest { @Test fun powerOfTwoCapacity_negativeValue() { - assertEquals(0, nextPowerOfTwoCapacity(-1)) - assertEquals(0, nextPowerOfTwoCapacity(-17)) + assertEquals(0, ByteArrayOutput.nextPowerOfTwoCapacity(-1)) + assertEquals(0, ByteArrayOutput.nextPowerOfTwoCapacity(-17)) } @Test fun powerOfTwoCapacity_zeroValue() { - assertEquals(0, nextPowerOfTwoCapacity(0)) + assertEquals(0, ByteArrayOutput.nextPowerOfTwoCapacity(0)) } @Test fun powerOfTwoCapacity_exactPowerOfTwo() { - assertEquals(16, nextPowerOfTwoCapacity(8)) - assertEquals(32, nextPowerOfTwoCapacity(16)) - assertEquals(64, nextPowerOfTwoCapacity(32)) + assertEquals(16, ByteArrayOutput.nextPowerOfTwoCapacity(8)) + assertEquals(32, ByteArrayOutput.nextPowerOfTwoCapacity(16)) + assertEquals(64, ByteArrayOutput.nextPowerOfTwoCapacity(32)) } @Test fun powerOfTwoCapacity_nonPowerOfTwo() { - assertEquals(16, nextPowerOfTwoCapacity(9)) - assertEquals(64, nextPowerOfTwoCapacity(33)) - assertEquals(128, nextPowerOfTwoCapacity(65)) + assertEquals(16, ByteArrayOutput.nextPowerOfTwoCapacity(9)) + assertEquals(64, ByteArrayOutput.nextPowerOfTwoCapacity(33)) + assertEquals(128, ByteArrayOutput.nextPowerOfTwoCapacity(65)) } @Test fun powerOfTwoCapacity_smallValues() { - assertEquals(2, nextPowerOfTwoCapacity(1)) - assertEquals(4, nextPowerOfTwoCapacity(2)) - assertEquals(4, nextPowerOfTwoCapacity(3)) + assertEquals(2, ByteArrayOutput.nextPowerOfTwoCapacity(1)) + assertEquals(4, ByteArrayOutput.nextPowerOfTwoCapacity(2)) + assertEquals(4, ByteArrayOutput.nextPowerOfTwoCapacity(3)) } @Test fun powerOfTwoCapacity_boundaryValues() { - assertEquals(0, nextPowerOfTwoCapacity(0)) - assertEquals(2, nextPowerOfTwoCapacity(1)) - assertEquals(4, nextPowerOfTwoCapacity(3)) - assertEquals(8, nextPowerOfTwoCapacity(5)) + assertEquals(0, ByteArrayOutput.nextPowerOfTwoCapacity(0)) + assertEquals(2, ByteArrayOutput.nextPowerOfTwoCapacity(1)) + assertEquals(4, ByteArrayOutput.nextPowerOfTwoCapacity(3)) + assertEquals(8, ByteArrayOutput.nextPowerOfTwoCapacity(5)) } @Test fun powerOfTwoCapacity_largeValues() { - assertEquals(1073741824, nextPowerOfTwoCapacity(536870912)) - assertEquals(1073741824, nextPowerOfTwoCapacity(1073741823)) - assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(1073741824)) - assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(1073741825)) - assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(Integer.MAX_VALUE-1)) - assertEquals(Integer.MAX_VALUE, nextPowerOfTwoCapacity(Integer.MAX_VALUE)) + assertEquals(1073741824, ByteArrayOutput.nextPowerOfTwoCapacity(536870912)) + assertEquals(1073741824, ByteArrayOutput.nextPowerOfTwoCapacity(1073741823)) + assertEquals(Integer.MAX_VALUE, ByteArrayOutput.nextPowerOfTwoCapacity(1073741824)) + assertEquals(Integer.MAX_VALUE, ByteArrayOutput.nextPowerOfTwoCapacity(1073741825)) + assertEquals(Integer.MAX_VALUE, ByteArrayOutput.nextPowerOfTwoCapacity(Integer.MAX_VALUE-1)) + assertEquals(Integer.MAX_VALUE, ByteArrayOutput.nextPowerOfTwoCapacity(Integer.MAX_VALUE)) } }