-
Notifications
You must be signed in to change notification settings - Fork 523
[Client Encryption] Add ArrayPool-backed pooled streams for reduced allocations in streaming encrypt/decrypt operations #5479
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
Open
adamnova
wants to merge
11
commits into
Azure:master
Choose a base branch
from
adamnova:feature/stream-processor-optimizations
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
…cryption - Add PooledMemoryStream: ArrayPool-backed MemoryStream replacement - Add PooledJsonSerializer: Optimized JSON serialization utilities - Add PooledStreamConfiguration: Centralized pooling configuration - Update SystemTextJsonStreamAdapter to use pooled streams - Update EncryptionProcessor to use pooled streams - Update MdeEncryptionProcessor to use pooled streams Benefits: - ~50% reduction in memory allocations for encrypt/decrypt operations - Reduced GC pressure through buffer reuse - Full Stream API compatibility - Secure buffer clearing on disposal (configurable) - Multi-target support (net8.0 + netstandard2.0)
Replaces raw BenchmarkDotNet output with a summarized analysis of memory allocation efficiency for stream-based encryption/decryption versus the Newtonsoft baseline. Adds tables, key observations, and instructions for running benchmarks, focusing on allocation metrics rather than execution time.
Added detailed XML remarks to PooledJsonSerializer, PooledMemoryStream, and PooledStreamConfiguration regarding thread safety, disposal, and performance/security considerations. Improved stream disposal and buffer management in StreamProcessor.Encryptor and SystemTextJsonStreamAdapter to ensure proper resource cleanup and prevent memory leaks. Fixed integer overflow checks in PooledMemoryStream for large write operations.
- PooledMemoryStreamTests: 586 lines, comprehensive stream operation tests - PooledJsonSerializerTests: 388 lines, serialization/deserialization tests - PooledJsonSerializerSecurityTests: Security-focused tests for depth limits and disposal Tests cover core functionality, error paths, async operations, disposal, and security hardening. Additional edge case and integration tests will follow in subsequent commits.
Tests SystemTextJsonStreamAdapter.cs:52 disposal path to prevent memory leaks when decryption returns null context. - Enhanced existing test with detailed comments and assertions - Added stress test with 1000 iterations to verify no memory leak - Both tests pass successfully
FIXES: - EncryptionProcessor.cs:263: Dispose PooledMemoryStream when context is null - MdeEncryptionProcessor.cs:146: Dispose PooledMemoryStream when context is null These fixes prevent ArrayPool exhaustion when decrypting non-encrypted payloads. TESTS ADDED: EncryptionProcessorTests.cs: - DecryptStreamAsync_UsesPooledMemoryStream_DisposesCorrectly - DecryptStreamAsync_WithNoEncryptionProperties_DisposesPooledMemoryStream - DecryptStreamAsync_UsesPooledJsonSerializer_DeserializesCorrectly - DecryptStreamAsync_RepeatedCalls_NoMemoryLeak (100 iterations stress test) MdeEncryptionProcessorTests.cs: - DecryptStreamAsync_CreatesPooledMemoryStream_SuccessPath - DecryptStreamAsync_WithEmptyEncryptedPaths_HandlesCorrectly - DecryptStreamAsync_RepeatedCalls_NoMemoryLeak (100 iterations stress test) StreamProcessorEncryptorTests.cs: - EncryptStreamAsync_TryFinallyBlock_DisposesResourcesOnSuccess - EncryptStreamAsync_ExceptionDuringParsing_DisposesResourcesInFinally - EncryptStreamAsync_CancellationRequested_DisposesResourcesInFinally - EncryptStreamAsync_RepeatedCallsWithExceptions_NoResourceLeak (100 iterations) All 11 new integration tests pass, verifying no memory leaks under stress.
NEW TEST FILES: - PooledResourceMemoryLeakTests.cs (6 tests) * Documents memory leak dangers and proper disposal patterns * Stress tests with 1000+ iterations to verify no ArrayPool exhaustion * Tests concurrent usage patterns * Tests rapid capacity growth scenarios - PooledResourceDisposalTests.cs (10 tests) * Verifies proper buffer return to ArrayPool * Tests clearOnReturn security feature (buffer zeroing) * Tests disposal edge cases (double dispose, zero capacity, empty stream) * Tests async disposal path * Verifies try-finally disposal in exception paths RESULTS: - All 306 tests pass (290 original + 16 new) - Comprehensive coverage of pooled resource lifecycle - Memory leak prevention patterns documented - Security features (buffer clearing) verified
Added calls to bufferWriter?.Dispose() before setting bufferWriter to null to ensure proper resource cleanup in the encryption process.
This commit addresses several critical issues in the pooled stream management: 1. Added back Debug.Assert(memoryStream.TryGetBuffer(out _)) in AeAesEncryptionProcessor - The assert was incorrectly removed in e4ee4b3 - TryGetBuffer() is internal and accessible within the same assembly - The assert validates the stream implementation before using ToArray() 2. Fixed PooledMemoryStream security gaps to prevent data leakage: - SetLength: Zero newly exposed regions when expanding length - Write: Zero gaps between current length and write position - Dispose: Clear entire capacity (not just length) to handle SetLength(0) - EnsureCapacity: Clear new buffer before use and old buffer before return 3. Fixed PooledJsonSerializer exception safety: - Added try-catch in SerializeToPooledArray to return rented buffer on exception - Enhanced documentation to clarify caller responsibility for buffer return 4. Added exception handling in all stream disposal paths: - SystemTextJsonStreamAdapter.EncryptAsync and DecryptAsync - MdeEncryptionProcessor.DecryptAsync - EncryptionProcessor.DecryptAsync All changes ensure that PooledMemoryStream instances are properly disposed even in error paths, preventing ArrayPool buffer leaks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
…configuration pattern Refactored PooledStreamConfiguration to use an immutable configuration object pattern for thread-safe access to configuration values. This eliminates race conditions from mutable static properties. Changes: - Convert PooledStreamConfiguration from static class to sealed class with immutable properties - Add Current property for thread-safe configuration reads - Add SetConfiguration() method for atomic configuration swaps - Replace mutable StreamInitialCapacity/BufferWriterInitialCapacity with init-only properties - Consolidate StreamProcessor.InitialBufferSize into PooledStreamConfiguration.StreamProcessorBufferSize - Update all usage sites to read from PooledStreamConfiguration.Current - Update test files to use SetConfiguration() pattern for test-specific configurations Thread-safety benefits: - Atomic reference swaps ensure readers always see consistent configuration - Immutable configuration objects prevent partial updates - No torn reads or memory visibility issues across threads - Tests can safely swap and restore configurations All 306 tests pass (net8.0) and 166 tests pass (net6.0). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR introduces memory-efficient pooled stream implementations for the Client Encryption Custom library to significantly reduce GC pressure during streaming encryption/decryption operations.
Key Changes:
New
PooledMemoryStream: AMemoryStreamreplacement that rents buffers fromArrayPool<byte>.Sharedand properly returns them on disposal. Includes:New
PooledJsonSerializer(.NET 8+): Optimized System.Text.Json serialization utilities using pooled buffers:SerializeToPooledStream<T>()- Returns aPooledMemoryStreamthat caller must disposeDeserializeFromStream/Spanwith MaxDepth=64 for DoS protectionNew
PooledStreamConfiguration(.NET 8+): Configurable initial capacities for pooled streams/buffersStreamProcessor improvements:
encryptionPayloadWriterandbufferWriterdisposal on exception pathsMemoryStreamwithPooledMemoryStreamin encrypt/decrypt hot pathsSystemTextJsonStreamAdapter fixes: Added disposal of
PooledMemoryStreamwhen decryption returns null contextBenchmark Results
Allocation reduction vs Newtonsoft baseline:
Type of change
Testing
PooledMemoryStream(read/write, seek, disposal, capacity growth)PooledJsonSerializertests (serialization round-trips, max depth limits, null handling)#4678