-
Notifications
You must be signed in to change notification settings - Fork 651
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
NIOSingleStepByteToMessageDecoder reentrancy safety #2881
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sneaky reentrancy :D
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
dc26cc8
to
7c3d82c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Rick!
Resolves #2639 |
7c3d82c
to
045674f
Compare
@@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if body
throws?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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!
045674f
to
736e244
Compare
### 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.
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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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` ([#​2879](https://redirect.github.com/apple/swift-nio/issues/2879))" by [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 [@​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 - [@​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) - [@​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) - [@​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) - [@​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>
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
anddecodeLast
are not).Result:
NIOSingleStepByteToMessageDecoder
can handle re-entrant processing calls.