How repeated descriptor bytes exhausted an AMQP decoder’s stack
Here, I am discussing the same bug that I have reported for Microsoft azure-uamqp-c. In my root cause analysis, I found that the value decoder was recursively creating inner decoders without limits on a chain.
Eventually, the process exhausted its stack and crashed.
MSRC confirmed that the issue was fixed and assessed it as moderate severity. The impact is Denial of Service.
Starting with the value decoder
My reproduction targeted commit 5481bd3 on Linux x86-64. I used a small C harness that read an input file into memory and passed it directly to amqpvalue_decode_bytes.
The relevant calls were:
AMQPVALUE_DECODER_HANDLE dec =
amqpvalue_decoder_create(on_value_decoded, NULL);
amqpvalue_decode_bytes(dec, buf, (size_t)sz);
amqpvalue_decoder_destroy(dec);
This isolated the value parser.
The reproduction shows a crash in the decoder. Whether an application exposes that path to an untrusted peer depends on how it receives and processes input.
One byte, another decoder
The interesting byte was 0x00, which is the constructor marker for an AMQP described value.
In the code path I examined, encountering that marker created an inner decoder to process the descriptor. The decoder state contained a handle to that child:
INTERNAL_DECODER_HANDLE inner_decoder;
The constructor handler created it with a call equivalent to the following excerpt:
internal_decoder_data->inner_decoder =
internal_decoder_create(
inner_decoder_callback,
internal_decoder_data,
descriptor,
true);
The described-value state then passed the remaining input into the child:
internal_decoder_decode_bytes(
internal_decoder_data->inner_decoder,
buffer,
size,
&inner_used_bytes);
That is another call to the same decoding function. If the child encountered another 0x00, it followed the same path.
The resulting call chain looked like this:
outer decoder
-> descriptor decoder
-> descriptor decoder
-> descriptor decoder
-> ...
In the tested revision there was no counter nor limit on this path.
Reaching the stack limit
My test inputs consisted of repeated 0x00 bytes followed by 0x40:
# Input used with the AddressSanitizer build: 26,501 bytes total.
b"\x00" * 26500 + b"\x40"
# Input used without AddressSanitizer: 88,001 bytes total.
b"\x00" * 88000 + b"\x40"
The trailing 0x40 is the AMQP null constructor. The failure came from the depth reached while processing the preceding descriptor markers.
With AddressSanitizer enabled, the smaller input produced:
feeding 26501 bytes
AddressSanitizer:DEADLYSIGNAL
ERROR: AddressSanitizer: stack-overflow
<empty stack>
The first report identified stack exhaustion but did not give me a useful backtrace. I rebuilt with -fno-omit-frame-pointer and ran with:
ASAN_OPTIONS=detect_leaks=0:fast_unwind_on_fatal=0 \
./test_driver_asan poc_asan.bin
That run recovered the repeating frames. Here is a shortened excerpt:
#5 internal_decoder_decode_bytes src/amqpvalue.c:4996
#6 internal_decoder_decode_bytes src/amqpvalue.c:5431
#7 internal_decoder_decode_bytes src/amqpvalue.c:5431
#8 internal_decoder_decode_bytes src/amqpvalue.c:5431
#9 internal_decoder_decode_bytes src/amqpvalue.c:5431
... repeated recursive calls ...
Based on the trace, it was easier to identify the mechanism than from the first crash report: the decoder was recursively calling itself in the same place.
I also tested a build without AddressSanitizer, using an 8 MiB stack limit:
ulimit -s 8192
./test_driver poc_stack_overflow.bin
That process terminated with a segmentation fault.
Disclosure and recognition
MSRC highlighted the quality of my report and how the reproduction helped the team develop a fix quickly. Microsoft included me in its Special Mentions in recognition of that contribution.