Skip to content
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

Prohibited using of zero and negative filed number in ProtoNumber and zero field numbers in input bytes #2766

Merged
merged 9 commits into from
Aug 23, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ internal fun SerialDescriptor.extractParameters(index: Int): ProtoDesc {
val annotation = annotations[i]
if (annotation is ProtoNumber) {
protoId = annotation.number
checkFieldNumber(protoId, i, this)
} else if (annotation is ProtoType) {
format = annotation.type
} else if (annotation is ProtoPacked) {
Expand Down Expand Up @@ -118,11 +119,21 @@ internal fun extractProtoId(descriptor: SerialDescriptor, index: Int, zeroBasedD
return ID_HOLDER_ONE_OF
} else if (annotation is ProtoNumber) {
result = annotation.number
// 0 or negative numbers are acceptable for enums
if (!zeroBasedDefault) {
checkFieldNumber(result, i, descriptor)
}
}
}
return result
}

private fun checkFieldNumber(fieldNumber: Int, propertyIndex: Int, descriptor: SerialDescriptor) {
if (fieldNumber <= 0) {
throw SerializationException("$fieldNumber is not allowed in ProtoNumber for property '${descriptor.getElementName(propertyIndex)}' of '${descriptor.serialName}', because protobuf support field numbers in range 1..${Int.MAX_VALUE}")
shanshin marked this conversation as resolved.
Show resolved Hide resolved
}
}

internal class ProtobufDecodingException(message: String, e: Throwable? = null) : SerializationException(message, e)

internal expect fun Int.reverseBytes(): Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ internal open class ProtobufDecoder(
* If we have reasonably small count of elements, try to build sequential
* array for the fast-path. Fast-path implies that elements are not marked with @ProtoId
* explicitly or are monotonic and incremental (maybe, 1-indexed)
*
* Initialize all elements, because there will always be one extra element as arrays are numbered from 0
* but in protobuf field number starts from 1.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either next line should be a complete sentence (it doesn't look like one now), or the dot shouldn't be here

* in the fast path array, which is missing from the descriptor
*/
val cache = IntArray(elements + 1)
val cache = IntArray(elements + 1) { -1 }
for (i in 0 until elements) {
val protoId = extractProtoId(descriptor, i, false)
// If any element is marked as ProtoOneOf,
Expand Down Expand Up @@ -307,6 +311,9 @@ internal open class ProtobufDecoder(
if (protoId == -1) { // EOF
return elementMarker.nextUnmarkedIndex()
}
if (protoId == 0) {
throw SerializationException("0 is not allowed as the protobuf field number in ${descriptor.serialName}, the input bytes may have been corrupted")
}
val index = getIndexByNum(protoId)
if (index == -1) { // not found
reader.skipElement()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ internal class ProtobufReader(private val input: ByteArrayInput) {
when (currentType) {
ProtoWireType.VARINT -> readInt(ProtoIntegerType.DEFAULT)
ProtoWireType.i64 -> readLong(ProtoIntegerType.FIXED)
ProtoWireType.SIZE_DELIMITED -> readByteArray()
ProtoWireType.SIZE_DELIMITED -> skipSizeDelimited()
ProtoWireType.i32 -> readInt(ProtoIntegerType.FIXED)
else -> throw ProtobufDecodingException("Unsupported start group or end group wire type: $currentType")
}
Expand All @@ -75,6 +75,13 @@ internal class ProtobufReader(private val input: ByteArrayInput) {
return readByteArrayNoTag()
}

fun skipSizeDelimited() {
assertWireType(ProtoWireType.SIZE_DELIMITED)
val length = decode32()
checkLength(length)
input.skipExactNBytes(length)
}

fun readByteArrayNoTag(): ByteArray {
val length = decode32()
checkLength(length)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ internal class ByteArrayInput(private var array: ByteArray, private val endIndex
return b
}


fun skipExactNBytes(bytesCount: Int) {
ensureEnoughBytes(bytesCount)
position += bytesCount
}

private fun ensureEnoughBytes(bytesCount: Int) {
if (bytesCount > availableBytes) {
throw SerializationException("Unexpected EOF, available $availableBytes bytes, requested: $bytesCount")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,15 @@ class AutoAssignIdsTest {
assertEquals(w1.a, w2.a)
assertEquals(w1.b, w2.b)
}

class NestedClass {
companion object
}

object X

@Test
fun testName() {
println(X::class.qualifiedName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.protobuf


import kotlinx.serialization.*
import kotlin.test.*

class InvalidFieldNumberTest {

@Serializable
data class Holder(val value: Int)

@Serializable
data class ListHolder(val value: List<Int>)

@Serializable
data class ZeroProtoNumber(@ProtoNumber(0) val value: Int)

@Serializable
data class NegativeProtoNumber(@ProtoNumber(-5) val value: Int)

@Test
fun testDeserializeZeroInput() {
assertFailsWithMessage<SerializationException>("0 is not allowed as the protobuf field number in kotlinx.serialization.protobuf.InvalidFieldNumberTest.Holder, the input bytes may have been corrupted") {
// first value with field number = 0
val hexString = "000f"
ProtoBuf.decodeFromHexString<Holder>(hexString)
}
}

@Test
fun testDeserializeZeroInputForElement() {
assertFailsWithMessage<SerializationException>("0 is not allowed as the protobuf field number in kotlinx.serialization.protobuf.InvalidFieldNumberTest.ListHolder, the input bytes may have been corrupted") {
// first element with field number = 0
val hexString = "000f"
ProtoBuf.decodeFromHexString<ListHolder>(hexString)
}
}

@Test
fun testSerializeZeroProtoNumber() {
assertFailsWithMessage<SerializationException>("0 is not allowed in ProtoNumber for property 'value' of 'kotlinx.serialization.protobuf.InvalidFieldNumberTest.ZeroProtoNumber', because protobuf support field numbers in range 1..2147483647") {
ProtoBuf.encodeToHexString(ZeroProtoNumber(42))
}
}

@Test
fun testDeserializeZeroProtoNumber() {
assertFailsWithMessage<SerializationException>("0 is not allowed in ProtoNumber for property 'value' of 'kotlinx.serialization.protobuf.InvalidFieldNumberTest.ZeroProtoNumber', because protobuf support field numbers in range 1..2147483647") {
ProtoBuf.decodeFromHexString<ZeroProtoNumber>("000f")
}
}

@Test
fun testSerializeNegativeProtoNumber() {
assertFailsWithMessage<SerializationException>("-5 is not allowed in ProtoNumber for property 'value' of 'kotlinx.serialization.protobuf.InvalidFieldNumberTest.NegativeProtoNumber', because protobuf support field numbers in range 1..2147483647") {
ProtoBuf.encodeToHexString(NegativeProtoNumber(42))
}
}

@Test
fun testDeserializeNegativeProtoNumber() {
assertFailsWithMessage<SerializationException>("-5 is not allowed in ProtoNumber for property 'value' of 'kotlinx.serialization.protobuf.InvalidFieldNumberTest.NegativeProtoNumber', because protobuf support field numbers in range 1..2147483647") {
ProtoBuf.decodeFromHexString<NegativeProtoNumber>("000f")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class PackedArraySerializerTest {

@Serializable
data class PackedStringCarrier(
@ProtoNumber(0)
@ProtoPacked
val s: List<String>
)
Expand Down Expand Up @@ -110,12 +109,12 @@ class PackedArraySerializerTest {
@Test
fun testEncodeAnnotatedStringList() {
val obj = PackedStringCarrier(listOf("aaa", "bbb", "ccc"))
val expectedHex = "020361616102036262620203636363"
val expectedHex = "0a036161610a036262620a03636363"
val encodedHex = ProtoBuf.encodeToHexString(obj)
assertEquals(expectedHex, encodedHex)
assertEquals(obj, ProtoBuf.decodeFromHexString<PackedStringCarrier>(expectedHex))

val invalidPackedHex = "020C036161610362626203636363"
val invalidPackedHex = "0a0C036161610362626203636363"
val decoded = ProtoBuf.decodeFromHexString<PackedStringCarrier>(invalidPackedHex)
val invalidString = "\u0003aaa\u0003bbb\u0003ccc"
assertEquals(PackedStringCarrier(listOf(invalidString)), decoded)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.protobuf


import kotlinx.serialization.*
import kotlin.test.*

class SkipFieldsTest {

@Serializable
data class Holder(val value: Int)

@Test
fun testSkipBigFieldNumber() {
// first value with id = 2047 and takes 2 bytes
val hexString = "f87f20082a"
val holder = ProtoBuf.decodeFromHexString<Holder>(hexString)
assertEquals(42, holder.value)
}

@Test
fun testSkipUnknownFiledNumberForString() {
// first value is size delimited (string) with id = 42
val hexString = "d2020c48656c6c6f20576f726c6421082a"
val holder = ProtoBuf.decodeFromHexString<Holder>(hexString)
assertEquals(42, holder.value)
}
}