Skip to content

Commit aa7804c

Browse files
authored
Don't mutate strings in Kestrel (#17556)
* Removed mutating of string-contents in BCryptHandle * Revert "Removed mutating of string-contents in BCryptHandle" This reverts commit 5ae80c2834471baf34d1e5a05a42e3cce1ff02d7. This is a .NET STandard 2.0 project, so no span is available by default. I think it's not worth it to add a reference to System.Memory-package just for this change. * Better perf for StringUtilities.TryGetAsciiString * Removed mutating of created string from HttpUtilities * Use static readonly span-actions as this gives a boost due to not having a null check for the compiler generated cached delegate * Debug Asserts * PR Feedback
1 parent 568e73e commit aa7804c

File tree

3 files changed

+249
-91
lines changed

3 files changed

+249
-91
lines changed

src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Buffers;
56
using System.Diagnostics;
67
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
79
using System.Text;
810
using Microsoft.AspNetCore.Http;
911
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
@@ -25,6 +27,8 @@ internal static partial class HttpUtilities
2527
private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
2628

2729
private static readonly UTF8EncodingSealed HeaderValueEncoding = new UTF8EncodingSealed();
30+
private static readonly SpanAction<char, IntPtr> _getHeaderName = GetHeaderName;
31+
private static readonly SpanAction<char, IntPtr> _getAsciiStringNonNullCharacters = GetAsciiStringNonNullCharacters;
2832

2933
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3034
private static void SetKnownMethod(ulong mask, ulong knownMethodUlong, HttpMethod knownMethod, int length)
@@ -81,52 +85,61 @@ private static unsafe ulong GetMaskAsLong(byte[] bytes)
8185
}
8286

8387
// The same as GetAsciiStringNonNullCharacters but throws BadRequest
88+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8489
public static unsafe string GetHeaderName(this ReadOnlySpan<byte> span)
8590
{
8691
if (span.IsEmpty)
8792
{
8893
return string.Empty;
8994
}
9095

91-
var asciiString = new string('\0', span.Length);
96+
fixed (byte* source = &MemoryMarshal.GetReference(span))
97+
{
98+
return string.Create(span.Length, new IntPtr(source), _getHeaderName);
99+
}
100+
}
92101

93-
fixed (char* output = asciiString)
94-
fixed (byte* buffer = span)
102+
private static unsafe void GetHeaderName(Span<char> buffer, IntPtr state)
103+
{
104+
fixed (char* output = &MemoryMarshal.GetReference(buffer))
95105
{
96106
// This version if AsciiUtilities returns null if there are any null (0 byte) characters
97107
// in the string
98-
if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length))
108+
if (!StringUtilities.TryGetAsciiString((byte*)state.ToPointer(), output, buffer.Length))
99109
{
100110
BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName);
101111
}
102112
}
103-
104-
return asciiString;
105113
}
106114

107115
public static string GetAsciiStringNonNullCharacters(this Span<byte> span)
108116
=> GetAsciiStringNonNullCharacters((ReadOnlySpan<byte>)span);
109117

118+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
110119
public static unsafe string GetAsciiStringNonNullCharacters(this ReadOnlySpan<byte> span)
111120
{
112121
if (span.IsEmpty)
113122
{
114123
return string.Empty;
115124
}
116125

117-
var asciiString = new string('\0', span.Length);
126+
fixed (byte* source = &MemoryMarshal.GetReference(span))
127+
{
128+
return string.Create(span.Length, new IntPtr(source), _getAsciiStringNonNullCharacters);
129+
}
130+
}
118131

119-
fixed (char* output = asciiString)
120-
fixed (byte* buffer = span)
132+
private static unsafe void GetAsciiStringNonNullCharacters(Span<char> buffer, IntPtr state)
133+
{
134+
fixed (char* output = &MemoryMarshal.GetReference(buffer))
121135
{
122136
// This version if AsciiUtilities returns null if there are any null (0 byte) characters
123137
// in the string
124-
if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length))
138+
if (!StringUtilities.TryGetAsciiString((byte*)state.ToPointer(), output, buffer.Length))
125139
{
126140
throw new InvalidOperationException();
127141
}
128142
}
129-
return asciiString;
130143
}
131144

132145
public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this Span<byte> span)
@@ -139,14 +152,12 @@ public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlyS
139152
return string.Empty;
140153
}
141154

142-
var resultString = new string('\0', span.Length);
143-
144-
fixed (char* output = resultString)
145-
fixed (byte* buffer = span)
155+
fixed (byte* source = &MemoryMarshal.GetReference(span))
146156
{
147-
// This version if AsciiUtilities returns null if there are any null (0 byte) characters
148-
// in the string
149-
if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length))
157+
var resultString = string.Create(span.Length, new IntPtr(source), s_getAsciiOrUtf8StringNonNullCharacters);
158+
159+
// If resultString is marked, perform UTF-8 encoding
160+
if (resultString[0] == '\0')
150161
{
151162
// null characters are considered invalid
152163
if (span.IndexOf((byte)0) != -1)
@@ -156,15 +167,32 @@ public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlyS
156167

157168
try
158169
{
159-
resultString = HeaderValueEncoding.GetString(buffer, span.Length);
170+
resultString = HeaderValueEncoding.GetString(span);
160171
}
161172
catch (DecoderFallbackException)
162173
{
163174
throw new InvalidOperationException();
164175
}
165176
}
177+
178+
return resultString;
179+
}
180+
}
181+
182+
private static readonly SpanAction<char, IntPtr> s_getAsciiOrUtf8StringNonNullCharacters = GetAsciiOrUTF8StringNonNullCharacters;
183+
184+
private static unsafe void GetAsciiOrUTF8StringNonNullCharacters(Span<char> buffer, IntPtr state)
185+
{
186+
fixed (char* output = &MemoryMarshal.GetReference(buffer))
187+
{
188+
// This version if AsciiUtilities returns null if there are any null (0 byte) characters
189+
// in the string
190+
if (!StringUtilities.TryGetAsciiString((byte*)state.ToPointer(), output, buffer.Length))
191+
{
192+
// Mark resultString for UTF-8 encoding
193+
output[0] = '\0';
194+
}
166195
}
167-
return resultString;
168196
}
169197

170198
public static string GetAsciiStringEscaped(this Span<byte> span, int maxChars)
@@ -283,7 +311,7 @@ public static HttpMethod GetKnownMethod(string value)
283311
{
284312
method = HttpMethod.Head;
285313
}
286-
else if(firstChar == 'P' && string.Equals(value, HttpMethods.Post, StringComparison.Ordinal))
314+
else if (firstChar == 'P' && string.Equals(value, HttpMethods.Post, StringComparison.Ordinal))
287315
{
288316
method = HttpMethod.Post;
289317
}
@@ -294,7 +322,7 @@ public static HttpMethod GetKnownMethod(string value)
294322
{
295323
method = HttpMethod.Trace;
296324
}
297-
else if(firstChar == 'P' && string.Equals(value, HttpMethods.Patch, StringComparison.Ordinal))
325+
else if (firstChar == 'P' && string.Equals(value, HttpMethods.Patch, StringComparison.Ordinal))
298326
{
299327
method = HttpMethod.Patch;
300328
}

src/Shared/Http2cat/Http2Utilities.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Http2Cat
2626
internal class Http2Utilities : IHttpHeadersHandler
2727
{
2828
public static ReadOnlySpan<byte> ClientPreface => new byte[24] { (byte)'P', (byte)'R', (byte)'I', (byte)' ', (byte)'*', (byte)' ', (byte)'H', (byte)'T', (byte)'T', (byte)'P', (byte)'/', (byte)'2', (byte)'.', (byte)'0', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n', (byte)'S', (byte)'M', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
29-
public static readonly int MaxRequestHeaderFieldSize = 16 * 1024;
29+
public const int MaxRequestHeaderFieldSize = 16 * 1024;
3030
public static readonly string FourKHeaderValue = new string('a', 4096);
3131
private static readonly Encoding HeaderValueEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
3232

0 commit comments

Comments
 (0)