Skip to content

Commit b6b2a97

Browse files
authored
Clear buffers before returning them to the ArrayPool (#124)
It is possible that sensitive data such as PII is being compressed or decompressed using Snappier. We don't want to return such data to the ArrayPool without zeroing it first, as it could create a security vulnerability if the buffer is reused by some other portion of an application that isn't properly handling the buffer. In some cases, we request the clear as part of the return. However, in others we know we've only used a subset of the buffer so we can optimize by only clearing the portion we've used. This change also removes some unnecessary try..finally blocks to return arrays to the pool during compression. Compression doesn't typically throw exceptions, and in any extreme corner cases we'll simply not return the array to the pool. This simplifies the code and provides a minor performance improvement.
1 parent 59d842b commit b6b2a97

File tree

7 files changed

+58
-43
lines changed

7 files changed

+58
-43
lines changed

Snappier/Internal/ByteArrayPoolMemoryOwner.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public void Dispose()
4545
byte[]? innerArray = _innerArray;
4646
if (innerArray is not null)
4747
{
48+
// Clear the used portion of the array before returning it to the pool
49+
Memory.Span.Clear();
50+
4851
_innerArray = null;
4952
Memory = default;
5053
ArrayPool<byte>.Shared.Return(innerArray);

Snappier/Internal/Helpers.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System;
2+
using System.Buffers;
23
using System.Buffers.Binary;
34
using System.Diagnostics;
45
using System.Runtime.CompilerServices;
56
using System.Runtime.InteropServices;
7+
68
#if NET6_0_OR_GREATER
79
using System.Numerics;
810
using System.Runtime.Intrinsics;
@@ -45,6 +47,23 @@ public static int MaxCompressedLength(int sourceBytes)
4547
return 32 + sourceBytes + sourceBytes / 6 + 1;
4648
}
4749

50+
// Constant for MaxCompressedLength when passed Constants.BlockSize, keep this in sync with the above method
51+
public const int MaxBlockCompressedLength = (int)(32 + Constants.BlockSize + Constants.BlockSize / 6 + 1);
52+
53+
/// <summary>
54+
/// Clears the array and returns it to the pool, clearing only the used portion of the array.
55+
/// This is a minor performance optimization to avoid clearing the entire array.
56+
/// </summary>
57+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
58+
public static void ClearAndReturn(byte[] array, int usedLength)
59+
{
60+
Debug.Assert(array is not null);
61+
Debug.Assert(usedLength >= 0);
62+
63+
array.AsSpan(0, usedLength).Clear();
64+
ArrayPool<byte>.Shared.Return(array);
65+
}
66+
4867
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4968
public static bool LeftShiftOverflows(byte value, int shift)
5069
{

Snappier/Internal/SnappyCompressor.cs

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,18 @@ public bool TryCompress(ReadOnlySpan<byte> input, Span<byte> output, out int byt
6060
// compress to a temporary buffer and copy the compressed data to the output span.
6161

6262
byte[] scratch = ArrayPool<byte>.Shared.Rent(maxOutput);
63-
try
63+
written = CompressFragment(fragment, scratch.AsSpan(), hashTable);
64+
if (output.Length < written)
6465
{
65-
written = CompressFragment(fragment, scratch.AsSpan(), hashTable);
66-
if (output.Length < written)
67-
{
68-
bytesWritten = 0;
69-
return false;
70-
}
71-
72-
scratch.AsSpan(0, written).CopyTo(output);
73-
}
74-
finally
75-
{
76-
ArrayPool<byte>.Shared.Return(scratch);
66+
Helpers.ClearAndReturn(scratch, written);
67+
bytesWritten = 0;
68+
return false;
7769
}
70+
71+
Span<byte> writtenScratch = scratch.AsSpan(0, written);
72+
writtenScratch.CopyTo(output);
73+
writtenScratch.Clear();
74+
ArrayPool<byte>.Shared.Return(scratch);
7875
}
7976

8077
output = output.Slice(written);
@@ -132,19 +129,17 @@ public void Compress(ReadOnlySequence<byte> input, IBufferWriter<byte> bufferWri
132129

133130
int fragmentLength = (int)fragment.Length;
134131
byte[] scratch = ArrayPool<byte>.Shared.Rent(fragmentLength);
135-
try
136-
{
137-
fragment.CopyTo(scratch);
138132

139-
CompressFragment(scratch.AsSpan(0, fragmentLength), bufferWriter);
133+
Span<byte> usedScratch = scratch.AsSpan(0, fragmentLength);
134+
fragment.CopyTo(usedScratch);
140135

141-
// Advance the length of the entire fragment
142-
input = input.Slice(position);
143-
}
144-
finally
145-
{
146-
ArrayPool<byte>.Shared.Return(scratch);
147-
}
136+
CompressFragment(usedScratch, bufferWriter);
137+
138+
usedScratch.Clear();
139+
ArrayPool<byte>.Shared.Return(scratch);
140+
141+
// Advance the length of the entire fragment
142+
input = input.Slice(position);
148143
}
149144
}
150145
}

Snappier/Internal/SnappyDecompressor.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,8 @@ private int? ExpectedLength
526526
{
527527
if (_lookbackBufferArray is not null)
528528
{
529-
ArrayPool<byte>.Shared.Return(_lookbackBufferArray);
529+
// Clear the used portion of the lookback buffer before returning
530+
Helpers.ClearAndReturn(_lookbackBufferArray, _lookbackPosition);
530531
}
531532

532533
if (BufferWriter is not null)
@@ -731,7 +732,9 @@ public void Dispose()
731732
{
732733
if (_lookbackBufferArray is not null)
733734
{
734-
ArrayPool<byte>.Shared.Return(_lookbackBufferArray);
735+
// Clear the used portion of the lookback buffer before returning
736+
Helpers.ClearAndReturn(_lookbackBufferArray, _lookbackPosition);
737+
735738
_lookbackBufferArray = null;
736739
_lookbackBuffer = default;
737740
}

Snappier/Internal/SnappyStreamCompressor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ private void EnsureBuffer()
276276
{
277277
// Allocate enough room for the stream header and block headers
278278
_outputBuffer ??=
279-
ArrayPool<byte>.Shared.Rent(Helpers.MaxCompressedLength((int) Constants.BlockSize) + 8 + SnappyHeader.Length);
279+
ArrayPool<byte>.Shared.Rent(Helpers.MaxBlockCompressedLength + 8 + SnappyHeader.Length);
280280

281281
// Allocate enough room for the stream header and block headers
282282
_inputBuffer ??= ArrayPool<byte>.Shared.Rent((int) Constants.BlockSize);
@@ -289,12 +289,12 @@ public void Dispose()
289289

290290
if (_outputBuffer is not null)
291291
{
292-
ArrayPool<byte>.Shared.Return(_outputBuffer);
292+
ArrayPool<byte>.Shared.Return(_outputBuffer, clearArray: true);
293293
_outputBuffer = null;
294294
}
295295
if (_inputBuffer is not null)
296296
{
297-
ArrayPool<byte>.Shared.Return(_inputBuffer);
297+
ArrayPool<byte>.Shared.Return(_inputBuffer, clearArray: true);
298298
_inputBuffer = null;
299299
}
300300
}

Snappier/Snappy.cs

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,16 @@ public static IMemoryOwner<byte> CompressToMemory(ReadOnlySpan<byte> input)
102102
{
103103
byte[] buffer = ArrayPool<byte>.Shared.Rent(GetMaxCompressedLength(input.Length));
104104

105-
try
105+
if (!TryCompress(input, buffer, out int length))
106106
{
107-
if (!TryCompress(input, buffer, out int length))
108-
{
109-
// Should be unreachable since we're allocating a buffer of the correct size.
110-
ThrowHelper.ThrowInvalidOperationException();
111-
}
107+
// The amount of data written is unknown, so clear the entire buffer when returning
108+
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
112109

113-
return new ByteArrayPoolMemoryOwner(buffer, length);
114-
}
115-
catch
116-
{
117-
ArrayPool<byte>.Shared.Return(buffer);
118-
throw;
110+
// Should be unreachable since we're allocating a buffer of the correct size.
111+
ThrowHelper.ThrowInvalidOperationException();
119112
}
113+
114+
return new ByteArrayPoolMemoryOwner(buffer, length);
120115
}
121116

122117
/// <summary>

Snappier/SnappyStream.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ protected override void Dispose(bool disposing)
494494
_buffer = null;
495495
if (!AsyncOperationIsActive)
496496
{
497-
ArrayPool<byte>.Shared.Return(buffer);
497+
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
498498
}
499499
}
500500

@@ -545,7 +545,7 @@ public override async ValueTask DisposeAsync()
545545
_buffer = null;
546546
if (!AsyncOperationIsActive)
547547
{
548-
ArrayPool<byte>.Shared.Return(buffer);
548+
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
549549
}
550550
}
551551
}

0 commit comments

Comments
 (0)