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

NIOSingleStepByteToMessageDecoder reentrancy safety #2881

Merged
merged 2 commits into from
Sep 13, 2024

Conversation

rnro
Copy link
Contributor

@rnro rnro commented Sep 12, 2024

Motivation:

NIOSingleStepByteToMessageDecoder calls out part way through its processing step to a user-provided closure which can cause re-entrant behavior which violates the assumption made in the code that if the buffer is non-empty at the start that will be true later in the method.

Modifications:

NIOSingleStepByteToMessageDecoder no longer assumes a non-empty buffer in its final phase of buffer management.

Further changes to protect against re-entrancy shouldn't be necessary because the outside call to messageReceiver is the only one which is permitted to be re-entrant (decode and decodeLast are not).

Result:

NIOSingleStepByteToMessageDecoder can handle re-entrant processing calls.

@rnro rnro added the 🔨 semver/patch No public API change. label Sep 12, 2024
Copy link
Contributor

@gjcairo gjcairo left a comment

Choose a reason for hiding this comment

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

Sneaky reentrancy :D

Sources/NIOCore/SingleStepByteToMessageDecoder.swift Outdated Show resolved Hide resolved
Sources/NIOCore/SingleStepByteToMessageDecoder.swift Outdated Show resolved Hide resolved
let decoder = ThrowingOnLastDecoder()
let b2mp = NIOSingleStepByteToMessageProcessor(decoder)
XCTAssertNoThrow(try b2mp.process(buffer: ByteBuffer(string: "1\n\n2\n3\n")) { line in
XCTAssertThrowsError(try b2mp.finishProcessing(seenEOF: true) { _ in }) { error in
Copy link
Contributor

Choose a reason for hiding this comment

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

This test is testing your fix, but unless I misunderstood your comment before the test, I think it's not happening in the way you expected.

XCTAssertThrowsError isn't actually throwing, so the closure passed to process(buffer:messageReceiver:) won't actually throw when executed.

This isn't all bad though, because that means that the buffer will be nil after the given closure, which will test your fix as the method will continue without erroring when force-unwrapping the buffer. However, that's not what you were set to test here, since we're not exiting part way.

I think it would make sense to keep this test (if we don't already have one that tests the messageReceiver being throwing), but removing the XCTAssertThrowsError so that finishProcessing actually throws, and we can assert that process throws when the inner closure fails.

However, I would definitely have a separate test to test the actual fix here: to do that you can get rid of the error being thrown on decodeLast: we just need to call finishProcessing but make sure it doesn't throw to trigger the re-entrancy bug.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was discussed out-of-band, but for future historians...

I did intend the behavior of XCTAssertThrowsError to check and then swallow the error thrown from the interior call to processing, but it should've been more obvious. Doing that allowed the interior call to leave self._buffer as nil.

Whilst investigating this in detail we discovered that self._buffer is nil because self._withNonCoWBuffer would set it to nil before calling the decode operation which then threw and then never replaced it. That seems dangerous and we should unconditionally replace the buffer after the decode operation whether or not it throws so I have made that change.

The test is left in place as a regression test with the catching of the error made explicit.

Motivation:

NIOSingleStepByteToMessageDecoder calls out part way through its
processing step to a user-provided closure which can cause re-entrant
behavior which violates the assumption made in the code that if the
buffer is non-empty at the start that will be true later in the method.

Modifications:

NIOSingleStepByteToMessageDecoder no longer assumes a non-empty buffer
in its final phase of buffer management.

Further changes to protect against re-entrancy shouldn't be necessary
because the outside call to `messageReceiver` is the only one which is
permitted to be re-entrant (`decode` and `decodeLast` are not).

Result:

NIOSingleStepByteToMessageDecoder can handle re-entrant processing
calls.
@rnro rnro force-pushed the NIOSingleStepByteToMessageDecoder_reentrancy branch from dc26cc8 to 7c3d82c Compare September 12, 2024 13:19
@rnro rnro requested a review from gjcairo September 12, 2024 13:26
Copy link
Contributor

@gjcairo gjcairo left a comment

Choose a reason for hiding this comment

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

Thanks Rick!

@rnro
Copy link
Contributor Author

rnro commented Sep 12, 2024

Resolves #2639

@rnro rnro force-pushed the NIOSingleStepByteToMessageDecoder_reentrancy branch from 7c3d82c to 045674f Compare September 12, 2024 13:40
@@ -233,8 +233,9 @@ public final class NIOSingleStepByteToMessageProcessor<Decoder: NIOSingleStepByt
}

self._buffer = nil // To avoid CoW
defer { self._buffer = buffer }

let result = try body(&buffer)
Copy link
Member

Choose a reason for hiding this comment

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

what if body throws?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is why we decided to move the self._buffer = buffer line into a defer block. This is actually what was triggering the original bug: body would throw but we'd never reinstate the original value of the buffer, so it would remain as nil, which caused issues when it happened reentrantly because the outer closure would come back to a context in which the buffer is now nil, and we were force-unwrapping it.
Rick mentioned this in #2881 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

🙈 I read the diff in reverse somehow haha. nice catch!

@rnro rnro force-pushed the NIOSingleStepByteToMessageDecoder_reentrancy branch from 045674f to 736e244 Compare September 12, 2024 15:38
@rnro rnro merged commit 530aa8d into apple:main Sep 13, 2024
28 of 29 checks passed
@rnro rnro deleted the NIOSingleStepByteToMessageDecoder_reentrancy branch September 13, 2024 10:43
ali-ahsan-ali pushed a commit to ali-ahsan-ali/swift-nio that referenced this pull request Sep 15, 2024
### Motivation:

`NIOSingleStepByteToMessageDecoder` calls out part way through its
processing step to a user-provided closure which can cause re-entrant
behavior which violates the assumption made in the code that if the
buffer is non-empty at the start that will be true later in the method.

### Modifications:

`NIOSingleStepByteToMessageDecoder` no longer assumes a non-empty buffer
in its final phase of buffer management.

Further changes to protect against re-entrancy shouldn't be necessary
because the outside call to `messageReceiver` is the only one which is
permitted to be re-entrant (`decode` and `decodeLast` are not).

### Result:

`NIOSingleStepByteToMessageDecoder` can handle re-entrant processing
calls.
cgrindel-self-hosted-renovate bot referenced this pull request in cgrindel/rules_swift_package_manager Sep 25, 2024
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [apple/swift-nio](https://redirect.github.com/apple/swift-nio) | minor
| `2.72.0` -> `2.73.0` |

---

### Release Notes

<details>
<summary>apple/swift-nio (apple/swift-nio)</summary>

###
[`v2.73.0`](https://redirect.github.com/apple/swift-nio/releases/tag/2.73.0)

[Compare
Source](https://redirect.github.com/apple/swift-nio/compare/2.72.0...2.73.0)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

#### What's Changed

##### SemVer Minor

- Make `ByteBuffer`'s description more useful by
[@&#8203;supersonicbyte](https://redirect.github.com/supersonicbyte) in
[https://github.com/apple/swift-nio/pull/2864](https://redirect.github.com/apple/swift-nio/pull/2864)
- Expose `UDP_MAX_SEGMENTS` via System by
[@&#8203;rnro](https://redirect.github.com/rnro) in
[https://github.com/apple/swift-nio/pull/2891](https://redirect.github.com/apple/swift-nio/pull/2891)
- Add new `ChannelOption` to get the amount of buffered outbound data in
the Channel by
[@&#8203;johnnzhou](https://redirect.github.com/johnnzhou) in
[https://github.com/apple/swift-nio/pull/2849](https://redirect.github.com/apple/swift-nio/pull/2849)
- Add an `AcceptBackoffHandler` to the async server bootstraps by
[@&#8203;FranzBusch](https://redirect.github.com/FranzBusch) in
[https://github.com/apple/swift-nio/pull/2782](https://redirect.github.com/apple/swift-nio/pull/2782)

##### SemVer Patch

- Adding a nicer description for `WebSocketFrame` by
[@&#8203;supersonicbyte](https://redirect.github.com/supersonicbyte) in
[https://github.com/apple/swift-nio/pull/2862](https://redirect.github.com/apple/swift-nio/pull/2862)
- Improving `description` and adding `debugDescription` to `NIOAny` by
[@&#8203;supersonicbyte](https://redirect.github.com/supersonicbyte) in
[https://github.com/apple/swift-nio/pull/2866](https://redirect.github.com/apple/swift-nio/pull/2866)
- Make FileChunk sendable by
[@&#8203;ali-ahsan-ali](https://redirect.github.com/ali-ahsan-ali) in
[https://github.com/apple/swift-nio/pull/2871](https://redirect.github.com/apple/swift-nio/pull/2871)
- Make `ByteBuffer.debugDescription` suitable for structural display by
[@&#8203;dnadoba](https://redirect.github.com/dnadoba) in
[https://github.com/apple/swift-nio/pull/2495](https://redirect.github.com/apple/swift-nio/pull/2495)
- Add support for WASILibc by
[@&#8203;MaxDesiatov](https://redirect.github.com/MaxDesiatov) in
[https://github.com/apple/swift-nio/pull/2671](https://redirect.github.com/apple/swift-nio/pull/2671)
- `NIOSingleStepByteToMessageDecoder` reentrancy safety by
[@&#8203;rnro](https://redirect.github.com/rnro) in
[https://github.com/apple/swift-nio/pull/2881](https://redirect.github.com/apple/swift-nio/pull/2881)
- Adopt `NIOThrowingAsyncSequenceProducer` by
[@&#8203;rnro](https://redirect.github.com/rnro) in
[https://github.com/apple/swift-nio/pull/2879](https://redirect.github.com/apple/swift-nio/pull/2879)
- Clamp buffer to maximum upon large write operation by
[@&#8203;ali-ahsan-ali](https://redirect.github.com/ali-ahsan-ali) in
[https://github.com/apple/swift-nio/pull/2745](https://redirect.github.com/apple/swift-nio/pull/2745)
- Revert "Adopt `NIOThrowingAsyncSequenceProducer`
([#&#8203;2879](https://redirect.github.com/apple/swift-nio/issues/2879))"
by [@&#8203;rnro](https://redirect.github.com/rnro) in
[https://github.com/apple/swift-nio/pull/2892](https://redirect.github.com/apple/swift-nio/pull/2892)
- Add concrete description for `EmbeddedEventLoop` by
[@&#8203;aryan-25](https://redirect.github.com/aryan-25) in
[https://github.com/apple/swift-nio/pull/2890](https://redirect.github.com/apple/swift-nio/pull/2890)
- Conditionally include linux/udp.h by
[@&#8203;rnro](https://redirect.github.com/rnro) in
[https://github.com/apple/swift-nio/pull/2894](https://redirect.github.com/apple/swift-nio/pull/2894)
- Work around a type checking error when using the Static Linux SDK by
[@&#8203;euanh](https://redirect.github.com/euanh) in
[https://github.com/apple/swift-nio/pull/2898](https://redirect.github.com/apple/swift-nio/pull/2898)

##### Other Changes

- \[CI] Run tests on push to main by
[@&#8203;FranzBusch](https://redirect.github.com/FranzBusch) in
[https://github.com/apple/swift-nio/pull/2868](https://redirect.github.com/apple/swift-nio/pull/2868)
- \[CI] License header support `.in` and `.cmake` files by
[@&#8203;FranzBusch](https://redirect.github.com/FranzBusch) in
[https://github.com/apple/swift-nio/pull/2870](https://redirect.github.com/apple/swift-nio/pull/2870)
- Include nanoseconds in assertion of timestamp for NIOFileSystem tests
by [@&#8203;gjcairo](https://redirect.github.com/gjcairo) in
[https://github.com/apple/swift-nio/pull/2869](https://redirect.github.com/apple/swift-nio/pull/2869)
- Correct the link of sswg-security at SECURITY.md by
[@&#8203;lamtrinhdev](https://redirect.github.com/lamtrinhdev) in
[https://github.com/apple/swift-nio/pull/2872](https://redirect.github.com/apple/swift-nio/pull/2872)
- Speculative fix for flakey AsyncTestingEventLoop test by
[@&#8203;simonjbeaumont](https://redirect.github.com/simonjbeaumont) in
[https://github.com/apple/swift-nio/pull/2873](https://redirect.github.com/apple/swift-nio/pull/2873)
- ci: Install shellcheck if not present in CI runner by
[@&#8203;simonjbeaumont](https://redirect.github.com/simonjbeaumont) in
[https://github.com/apple/swift-nio/pull/2882](https://redirect.github.com/apple/swift-nio/pull/2882)
- ci: Use ${GITHUB_BASE_REF} as treeish for checking API break by
[@&#8203;simonjbeaumont](https://redirect.github.com/simonjbeaumont) in
[https://github.com/apple/swift-nio/pull/2883](https://redirect.github.com/apple/swift-nio/pull/2883)
- ci: Refer to nested reusable workflows using remote variant by
[@&#8203;simonjbeaumont](https://redirect.github.com/simonjbeaumont) in
[https://github.com/apple/swift-nio/pull/2884](https://redirect.github.com/apple/swift-nio/pull/2884)
- \[CI] Fix pull request label workflow by
[@&#8203;FranzBusch](https://redirect.github.com/FranzBusch) in
[https://github.com/apple/swift-nio/pull/2885](https://redirect.github.com/apple/swift-nio/pull/2885)

#### New Contributors

- [@&#8203;ali-ahsan-ali](https://redirect.github.com/ali-ahsan-ali)
made their first contribution in
[https://github.com/apple/swift-nio/pull/2871](https://redirect.github.com/apple/swift-nio/pull/2871)
- [@&#8203;aryan-25](https://redirect.github.com/aryan-25) made their
first contribution in
[https://github.com/apple/swift-nio/pull/2890](https://redirect.github.com/apple/swift-nio/pull/2890)
- [@&#8203;johnnzhou](https://redirect.github.com/johnnzhou) made their
first contribution in
[https://github.com/apple/swift-nio/pull/2849](https://redirect.github.com/apple/swift-nio/pull/2849)
- [@&#8203;euanh](https://redirect.github.com/euanh) made their first
contribution in
[https://github.com/apple/swift-nio/pull/2898](https://redirect.github.com/apple/swift-nio/pull/2898)

**Full Changelog**:
apple/swift-nio@2.72.0...2.73.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Renovate
Bot](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC45NC4xIiwidXBkYXRlZEluVmVyIjoiMzguOTQuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: cgrindel-self-hosted-renovate[bot] <139595543+cgrindel-self-hosted-renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants