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

Fix buffer overflow and data corruption when a type has more than 5 layers of nesting #3203

Merged
merged 1 commit into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -708,13 +708,13 @@ public final class ProtoWriter {
}

if let isProto3 = isProto3 {
messageStackIndex += 1
if messageStackIndex >= messageStackCapacity {
if messageStackIndex + 1 >= messageStackCapacity {
expandMessageStack()
}
let frame = MessageFrame(
isProto3: isProto3
)
messageStackIndex += 1
messageStack.advanced(by: messageStackIndex).initialize(to: frame)

// Cache this value as an ivar for quick access.
Expand Down
3 changes: 3 additions & 0 deletions wire-runtime-swift/src/test/proto/nested4.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
* limitations under the License.
*/
syntax = "proto2";
import "nested5.proto";

message Nested4 {
required string name = 1;

optional Nested5 nested = 2;
}

25 changes: 25 additions & 0 deletions wire-runtime-swift/src/test/proto/nested5.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2024 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
syntax = "proto2";

import "nested6.proto";

message Nested5 {
required string name = 1;

optional Nested6 nested = 2;
}

21 changes: 21 additions & 0 deletions wire-runtime-swift/src/test/proto/nested6.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2024 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
syntax = "proto2";

message Nested6 {
required string name = 1;
}

50 changes: 50 additions & 0 deletions wire-runtime-swift/src/test/swift/ProtoWriterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,56 @@ final class ProtoWriterTests: XCTestCase {
""")
}

func testEncodeMessageWithStackExpansion() throws {
let writer = ProtoWriter()
let message = Nested1(name: "name1") { v1 in
v1.nested = Nested2(name: "name2") { v2 in
v2.nested = Nested3(name: "name3") { v3 in
v3.nested = Nested4(name: "name4") { v4 in
v4.nested = Nested5(name: "name5") { v5 in
v5.nested = Nested6(name: "name6")
}
}
}
}
}

try writer.encode(tag: 1, value: message)

assertBufferEqual(writer, """
Copy link
Member Author

@amorde amorde Dec 6, 2024

Choose a reason for hiding this comment

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

this test fails without the change.

here's the bytes after the fix, followed by the bytes before the fix. before the fix one of the bytes is actually missing - the 0A (Tag 1 | Length Delimited) byte that preceeds the name2 string value

  0A 34 0A 05 6E616D6531 12 2B 0A 05 6E616D6532 12 22 0A 05 6E616D6533 12 19 0A 05 6E616D65 3412 10 0A 05 6E616D6535 1207 0A 05 6E616D6536
  0A 34 0A 05 6E616D6531 12 2B XX 05 6E616D6532 12 22 0A 05 6E616D6533 12 19 0A 05 6E616D65 3412 10 0A 05 6E616D6535 1207 0A 05 6E616D6536
                                 \
                                  \_ missing byte

Copy link

Choose a reason for hiding this comment

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

this doesn't seem to repro for me... did you have to do anything special? what toolchain were you using?

Copy link
Member Author

Choose a reason for hiding this comment

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

I was using Xcode 16.0

Copy link
Member Author

Choose a reason for hiding this comment

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

what part doesn't repro for you?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I can't seem to repro the corrupted data either. Perhaps I encountered this while testing some other fix? I'm not sure. I was copying those hex strings from the assertions that were failing 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Assuming this was a false signal I encountered then its a relief that the actual data that was encoded was not impacted

Copy link

Choose a reason for hiding this comment

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

what part doesn't repro for you?

the test still passes even if i run with the implementation change as it was vs the fixed version.

Assuming this was a false signal I encountered then its a relief that the actual data that was encoded was not impacted

agreed. but does the test actually fail then without the change? obviously good to have it regardless so that something is exercising the 'grow the buffer' path, but also makes me feel it might provide less safety than it might aspire to. ideally these tests should probably run in a mode with ASAN on as part of CI.

0A // (Tag 1 | Length Delimited)
34 // Length 52 for Nested1
0A // (Tag 1 | Length Delimited)
05 // Length 5 for name1
6E616D6531 // UTF-8 Value "name1"
12 // (Tag 2 | Length Delimited)
2B // Length 43
0A // (Tag 1 | Length Delimited)
05 // Length 5 for name2
6E616D6532 // UTF-8 Value "name2"
12 // (Tag 2 | Length Delimited)
22 // Length 34
0A // (Tag 1 | Length Delimited)
05 // Length 5 for name3
6E616D6533 // UTF-8 Value "name3"
12 // (Tag 2 | Length Delimited)
19 // Length 25
0A // (Tag 1 | Length Delimited)
05 // Length 5 for name4
6E616D6534 // UTF-8 Value "name4"
12 // (Tag 2 | Length Delimited)
10 // Length 16
0A // (Tag 1 | Length Delimited)
05 // Length 5 for name5
6E616D6535 // UTF-8 Value "name5"
12 // (Tag 2 | Length Delimited)
07 // Length 7
0A // (Tag 1 | Length Delimited)
05 // Length 5 for name6
6E616D6536 // UTF-8 Value "name6"
""")
}

func testEncodeString() throws {
let writer = ProtoWriter()
try writer.encode(tag: 1, value: "foo")
Expand Down
Loading