Skip to content

Commit b389117

Browse files
[release/6.0] Fix HTTP/2 header decoder buffer allocation (#47950)
* Fix HTTTP/2 header decoder buffer allocation * Fix HPack test warnings. --------- Co-authored-by: Aditya Mandaleeka <[email protected]>
1 parent 832ccdc commit b389117

File tree

3 files changed

+122
-11
lines changed

3 files changed

+122
-11
lines changed

.editorconfig

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,12 @@ dotnet_diagnostic.CA1829.severity = warning
122122
dotnet_diagnostic.CA1830.severity = warning
123123

124124
# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
125-
# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
126-
# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
127125
dotnet_diagnostic.CA1831.severity = warning
126+
127+
# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
128128
dotnet_diagnostic.CA1832.severity = warning
129+
130+
# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
129131
dotnet_diagnostic.CA1833.severity = warning
130132

131133
# CA1834: Consider using 'StringBuilder.Append(char)' when applicable
@@ -225,6 +227,12 @@ dotnet_diagnostic.CA1826.severity = suggestion
225227
dotnet_diagnostic.CA1827.severity = suggestion
226228
# CA1829: Use Length/Count property instead of Count() when available
227229
dotnet_diagnostic.CA1829.severity = suggestion
230+
# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
231+
dotnet_diagnostic.CA1831.severity = suggestion
232+
# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
233+
dotnet_diagnostic.CA1832.severity = suggestion
234+
# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
235+
dotnet_diagnostic.CA1833.severity = suggestion
228236
# CA1834: Consider using 'StringBuilder.Append(char)' when applicable
229237
dotnet_diagnostic.CA1834.severity = suggestion
230238
# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'

src/Shared/runtime/Http2/Hpack/HPackDecoder.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,11 @@ private void DecodeInternal(ReadOnlySpan<byte> data, IHttpHeadersHandler handler
187187
// will no longer be valid.
188188
if (_headerNameRange != null)
189189
{
190-
EnsureStringCapacity(ref _headerNameOctets);
190+
EnsureStringCapacity(ref _headerNameOctets, _headerNameLength);
191191
_headerName = _headerNameOctets;
192192

193193
ReadOnlySpan<byte> headerBytes = data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length);
194194
headerBytes.CopyTo(_headerName);
195-
_headerNameLength = headerBytes.Length;
196195
_headerNameRange = null;
197196
}
198197
}
@@ -427,6 +426,7 @@ private void ParseHeaderName(ReadOnlySpan<byte> data, ref int currentIndex, IHtt
427426
{
428427
// Fast path. Store the range rather than copying.
429428
_headerNameRange = (start: currentIndex, count);
429+
_headerNameLength = _stringLength;
430430
currentIndex += count;
431431

432432
_state = State.HeaderValueLength;
@@ -616,11 +616,12 @@ int Decode(ref byte[] dst)
616616
_state = nextState;
617617
}
618618

619-
private void EnsureStringCapacity(ref byte[] dst)
619+
private void EnsureStringCapacity(ref byte[] dst, int stringLength = -1)
620620
{
621-
if (dst.Length < _stringLength)
621+
stringLength = stringLength >= 0 ? stringLength : _stringLength;
622+
if (dst.Length < stringLength)
622623
{
623-
dst = new byte[Math.Max(_stringLength, Math.Min(dst.Length * 2, _maxHeadersLength))];
624+
dst = new byte[Math.Max(stringLength, Math.Min(dst.Length * 2, _maxHeadersLength))];
624625
}
625626
}
626627

src/Shared/test/Shared.Tests/runtime/Http2/HPackDecoderTest.cs

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,13 @@ public class HPackDecoderTests
4646

4747
private const string _headerNameString = "new-header";
4848

49+
// On purpose longer than 4096 (DefaultStringOctetsSize from HPackDecoder) to trigger https://github.com/dotnet/runtime/issues/78516
50+
private static readonly string _literalHeaderNameString = string.Concat(Enumerable.Range(0, 4100).Select(c => (char)('a' + (c % 26))));
51+
4952
private static readonly byte[] _headerNameBytes = Encoding.ASCII.GetBytes(_headerNameString);
5053

54+
private static readonly byte[] _literalHeaderNameBytes = Encoding.ASCII.GetBytes(_literalHeaderNameString);
55+
5156
// n e w - h e a d e r *
5257
// 10101000 10111110 00010110 10011100 10100011 10010000 10110110 01111111
5358
private static readonly byte[] _headerNameHuffmanBytes = new byte[] { 0xa8, 0xbe, 0x16, 0x9c, 0xa3, 0x90, 0xb6, 0x7f };
@@ -64,6 +69,12 @@ public class HPackDecoderTests
6469
.Concat(_headerNameBytes)
6570
.ToArray();
6671

72+
// size = 4096 ==> 0x7f, 0x81, 0x1f (7+) prefixed integer
73+
// size = 4100 ==> 0x7f, 0x85, 0x1f (7+) prefixed integer
74+
private static readonly byte[] _literalHeaderName = new byte[] { 0x7f, 0x85, 0x1f } // 4100
75+
.Concat(_literalHeaderNameBytes)
76+
.ToArray();
77+
6778
private static readonly byte[] _headerNameHuffman = new byte[] { (byte)(0x80 | _headerNameHuffmanBytes.Length) }
6879
.Concat(_headerNameHuffmanBytes)
6980
.ToArray();
@@ -392,6 +403,101 @@ public void DecodesLiteralHeaderFieldNeverIndexed_IndexedName_OutOfRange_Error()
392403
Assert.Empty(_handler.DecodedHeaders);
393404
}
394405

406+
[Fact]
407+
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_SingleBuffer()
408+
{
409+
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
410+
.Concat(_literalHeaderName)
411+
.Concat(_headerValue)
412+
.ToArray();
413+
414+
_decoder.Decode(encoded, endHeaders: true, handler: _handler);
415+
416+
Assert.Single(_handler.DecodedHeaders);
417+
Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
418+
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
419+
}
420+
421+
[Fact]
422+
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_NameLengthBrokenIntoSeparateBuffers()
423+
{
424+
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
425+
.Concat(_literalHeaderName)
426+
.Concat(_headerValue)
427+
.ToArray();
428+
429+
_decoder.Decode(encoded[..1], endHeaders: false, handler: _handler);
430+
_decoder.Decode(encoded[1..], endHeaders: true, handler: _handler);
431+
432+
Assert.Single(_handler.DecodedHeaders);
433+
Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
434+
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
435+
}
436+
437+
[Fact]
438+
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_NameBrokenIntoSeparateBuffers()
439+
{
440+
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
441+
.Concat(_literalHeaderName)
442+
.Concat(_headerValue)
443+
.ToArray();
444+
445+
_decoder.Decode(encoded[..(_literalHeaderNameString.Length / 2)], endHeaders: false, handler: _handler);
446+
_decoder.Decode(encoded[(_literalHeaderNameString.Length / 2)..], endHeaders: true, handler: _handler);
447+
448+
Assert.Single(_handler.DecodedHeaders);
449+
Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
450+
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
451+
}
452+
453+
[Fact]
454+
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_NameAndValueBrokenIntoSeparateBuffers()
455+
{
456+
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
457+
.Concat(_literalHeaderName)
458+
.Concat(_headerValue)
459+
.ToArray();
460+
461+
_decoder.Decode(encoded[..^_headerValue.Length], endHeaders: false, handler: _handler);
462+
_decoder.Decode(encoded[^_headerValue.Length..], endHeaders: true, handler: _handler);
463+
464+
Assert.Single(_handler.DecodedHeaders);
465+
Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
466+
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
467+
}
468+
469+
[Fact]
470+
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_ValueLengthBrokenIntoSeparateBuffers()
471+
{
472+
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
473+
.Concat(_literalHeaderName)
474+
.Concat(_headerValue)
475+
.ToArray();
476+
477+
_decoder.Decode(encoded[..^(_headerValue.Length - 1)], endHeaders: false, handler: _handler);
478+
_decoder.Decode(encoded[^(_headerValue.Length - 1)..], endHeaders: true, handler: _handler);
479+
480+
Assert.Single(_handler.DecodedHeaders);
481+
Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
482+
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
483+
}
484+
485+
[Fact]
486+
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_ValueBrokenIntoSeparateBuffers()
487+
{
488+
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
489+
.Concat(_literalHeaderName)
490+
.Concat(_headerValue)
491+
.ToArray();
492+
493+
_decoder.Decode(encoded[..^(_headerValueString.Length / 2)], endHeaders: false, handler: _handler);
494+
_decoder.Decode(encoded[^(_headerValueString.Length / 2)..], endHeaders: true, handler: _handler);
495+
496+
Assert.Single(_handler.DecodedHeaders);
497+
Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
498+
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
499+
}
500+
395501
[Fact]
396502
public void DecodesDynamicTableSizeUpdate()
397503
{
@@ -500,10 +606,8 @@ public void DecodesStringLength_ExceedsLimit_Throws()
500606
string string8191 = new string('a', MaxHeaderFieldSize - 1);
501607
string string8193 = new string('a', MaxHeaderFieldSize + 1);
502608
string string8194 = new string('a', MaxHeaderFieldSize + 2);
503-
504609
var bytes = new byte[3];
505610
var success = IntegerEncoder.Encode(8194, 7, bytes, out var written);
506-
507611
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
508612
.Concat(new byte[] { 0x7f, 0x80, 0x3f }) // 8191 encoded with 7-bit prefix, no Huffman encoding
509613
.Concat(Encoding.ASCII.GetBytes(string8191))
@@ -520,14 +624,12 @@ public void DecodesStringLength_ExceedsLimit_Throws()
520624
.Concat(new byte[] { 0x7f, 0x83, 0x3f }) // 8194 encoded with 7-bit prefix, no Huffman encoding
521625
.Concat(Encoding.ASCII.GetBytes(string8194))
522626
.ToArray();
523-
524627
var ex = Assert.Throws<HPackDecodingException>(() => decoder.Decode(encoded, endHeaders: true, handler: _handler));
525628
Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, MaxHeaderFieldSize + 1), ex.Message);
526629
Assert.Equal(string8191, _handler.DecodedHeaders[string8191]);
527630
Assert.Equal(string8193, _handler.DecodedHeaders[string8193]);
528631
Assert.False(_handler.DecodedHeaders.ContainsKey(string8194));
529632
}
530-
531633
[Fact]
532634
public void DecodesStringLength_IndividualBytes()
533635
{

0 commit comments

Comments
 (0)