Skip to content

Commit 7cacdee

Browse files
authored
Fix non-thread-safe use of Random in MultipartFormDataBinaryContent (#36)
* Fix non-thread-safe use of Random in MultipartFormDataBinaryContent MultipartFormDataBinaryContent has a static Random instance that's being used without a lock. This locks around each access (an alternative would be to use and lazily-initialize on each access a [ThreadStatic] Random). While fixing that, also improved a few things: - We don't need to create a char[] for the boundary values; the const string can be used directly. - For netstandard2.0, we don't need to allocate a char[]; a stackalloc'd span can be used directly. - For .NET, we don't need a stackalloc, either; we can use string.Create and write directly into the new string instance. - For .NET, we don't need a dedicated Random instance, either; we can use Random.Shared. - For .NET, Random allows writing into a byte span, so we don't need the byte[] allocation either and can stackalloc it. - For .NET, just %'ing by the string length produces as good or better code than using a manually-computed mask. * Address PR feedback
1 parent 674e0f7 commit 7cacdee

File tree

1 file changed

+36
-20
lines changed

1 file changed

+36
-20
lines changed

src/Utility/MultipartFormDataBinaryContent.cs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ internal class MultipartFormDataBinaryContent : BinaryContent
1414
{
1515
private readonly MultipartFormDataContent _multipartContent;
1616

17-
private static Random _random = new();
18-
private static readonly char[] _boundaryValues = "0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".ToCharArray();
17+
private const int BoundaryLength = 70;
18+
private const string BoundaryValues = "0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
1919

2020
public MultipartFormDataBinaryContent()
2121
{
@@ -120,27 +120,44 @@ public static void AddContentTypeHeader(HttpContent content, string contentType)
120120
content.Headers.ContentType = header;
121121
}
122122

123+
#if NET6_0_OR_GREATER
124+
private static string CreateBoundary() =>
125+
string.Create(BoundaryLength, 0, (chars, _) =>
126+
{
127+
Span<byte> random = stackalloc byte[BoundaryLength];
128+
Random.Shared.NextBytes(random);
129+
130+
for (int i = 0; i < chars.Length; i++)
131+
{
132+
chars[i] = BoundaryValues[random[i] % BoundaryValues.Length];
133+
}
134+
});
135+
#else
136+
private static readonly Random _random = new();
137+
123138
private static string CreateBoundary()
124139
{
125-
Span<char> chars = new char[70];
140+
Span<char> chars = stackalloc char[BoundaryLength];
126141

127-
byte[] random = new byte[70];
128-
_random.NextBytes(random);
129-
130-
// The following will sample evenly from the possible values.
131-
// This is important to ensuring that the odds of creating a boundary
132-
// that occurs in any content part are astronomically small.
133-
int mask = 255 >> 2;
142+
byte[] random = new byte[BoundaryLength];
143+
lock (_random)
144+
{
145+
_random.NextBytes(random);
146+
}
134147

135-
Debug.Assert(_boundaryValues.Length - 1 == mask);
148+
// Instead of `% BoundaryValues.Length` as is used above, use a mask to achieve the same result.
149+
// `% BoundaryValues.Length` is optimized to the equivalent on .NET Core but not on .NET Framework.
150+
const int Mask = 255 >> 2;
151+
Debug.Assert(BoundaryValues.Length - 1 == Mask);
136152

137-
for (int i = 0; i < 70; i++)
153+
for (int i = 0; i < chars.Length; i++)
138154
{
139-
chars[i] = _boundaryValues[random[i] & mask];
155+
chars[i] = BoundaryValues[random[i] & Mask];
140156
}
141157

142158
return chars.ToString();
143159
}
160+
#endif
144161

145162
public override bool TryComputeLength(out long length)
146163
{
@@ -158,22 +175,21 @@ public override bool TryComputeLength(out long length)
158175

159176
public override void WriteTo(Stream stream, CancellationToken cancellationToken = default)
160177
{
161-
// TODO: polyfill sync-over-async for netstandard2.0 for Azure clients.
162-
// Tracked by https://github.com/Azure/azure-sdk-for-net/issues/42674
163-
164-
#if NET6_0_OR_GREATER
178+
#if NET5_0_OR_GREATER
165179
_multipartContent.CopyTo(stream, default, cancellationToken);
166180
#else
167-
_multipartContent.CopyToAsync(stream).GetAwaiter().GetResult();
181+
// TODO: polyfill sync-over-async for netstandard2.0 for Azure clients.
182+
// Tracked by https://github.com/Azure/azure-sdk-for-net/issues/42674
183+
_multipartContent.CopyToAsync(stream).GetAwaiter().GetResult();
168184
#endif
169185
}
170186

171187
public override async Task WriteToAsync(Stream stream, CancellationToken cancellationToken = default)
172188
{
173-
#if NET6_0_OR_GREATER
189+
#if NET5_0_OR_GREATER
174190
await _multipartContent.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
175191
#else
176-
await _multipartContent.CopyToAsync(stream).ConfigureAwait(false);
192+
await _multipartContent.CopyToAsync(stream).ConfigureAwait(false);
177193
#endif
178194
}
179195

0 commit comments

Comments
 (0)