Skip to content

Commit baa1662

Browse files
BrunoBlanesManickaP
andcommitted
Fix HTTP3 header decoder buffer allocation (dotnet#78862)
* Add test for literal field without name reference * Fix header name buffer allocation * Add more tests * Unified QPackDecoderTest test files * Fix variable name * Fixed HPackDecoder and ported QPack tests * Feedback --------- Co-authored-by: ManickaP <mapichov@microsoft.com>
1 parent 4af6023 commit baa1662

File tree

2 files changed

+112
-5
lines changed

2 files changed

+112
-5
lines changed

src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackDecoder.cs

+6-5
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, dst.Length * 2)];
624+
dst = new byte[Math.Max(stringLength, dst.Length * 2)];
624625
}
625626
}
626627

src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http2/HPackDecoderTest.cs

+106
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.Equal(1, _handler.DecodedHeaders.Count);
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.Equal(1, _handler.DecodedHeaders.Count);
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.Equal(1, _handler.DecodedHeaders.Count);
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.Equal(1, _handler.DecodedHeaders.Count);
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.Equal(1, _handler.DecodedHeaders.Count);
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.Equal(1, _handler.DecodedHeaders.Count);
497+
Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
498+
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
499+
}
500+
395501
[Fact]
396502
public void DecodesDynamicTableSizeUpdate()
397503
{

0 commit comments

Comments
 (0)