Skip to content

Commit 195e549

Browse files
Merge pull request #16994 from rabbitmq/amqp-parser
Encode AMQP 1.0 arrays of zero-octet elements as opaque values
2 parents 83d29a2 + 3f0f67d commit 195e549

5 files changed

Lines changed: 112 additions & 60 deletions

File tree

deps/amqp10_common/src/amqp10_binary_generator.erl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,9 @@ generate2(array, {array, Type, List}) ->
245245
Array = [constructor(Type),
246246
[generate2(Type, I) || I <- List]],
247247
S = iolist_size(Array),
248-
[<<(S + 4):32, Count:32>>, Array].
248+
[<<(S + 4):32, Count:32>>, Array];
249+
generate2(array, {as_is, 16#f0, Bin}) ->
250+
Bin;
251+
generate2(array, {as_is, 16#e0, <<_Size:8, Count:8, Type>>}) ->
252+
%% Nested array elements are always framed as array32, see constructor/1
253+
<<5:32, Count:32, Type>>.

deps/amqp10_common/src/amqp10_binary_parser.erl

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@
3535
-define(DESCRIPTOR_CODE_DATA, 16#75).
3636
-define(DESCRIPTOR_CODE_AMQP_SEQUENCE, 16#76).
3737
-define(DESCRIPTOR_CODE_AMQP_VALUE, 16#77).
38-
-define(MAX_ZERO_WIDTH_ARRAY_COUNT, 10_000).
39-
4038

4139
%% server_mode is a special parsing mode used by RabbitMQ when parsing
4240
%% AMQP message sections from an AMQP client. This mode:
@@ -124,32 +122,40 @@ parse(<<Type, _/binary>>, B) ->
124122

125123
parse_array(UnitSize, Bin) ->
126124
<<Count:UnitSize, Bin1/binary>> = Bin,
127-
parse_array1(Count, Bin1).
125+
parse_array1(UnitSize, Count, Bin1).
128126

129-
parse_array1(Count, <<?DESCRIBED, Rest/binary>>) ->
127+
parse_array1(UnitSize, Count, <<?DESCRIBED, Rest/binary>>) ->
130128
{Descriptor, B1} = parse(Rest),
131129
<<_ParsedDescriptorBin:B1/binary, Rest1/binary>> = Rest,
132-
{array, Type, List} = parse_array1(Count, Rest1),
133-
Values = lists:map(fun (Value) ->
134-
{described, Descriptor, Value}
135-
end, List),
136-
% this format cannot represent an empty array of described types
137-
{array, {described, Descriptor, Type}, Values};
138-
parse_array1(Count, <<Type, ArrayBin/binary>>)
130+
case parse_array1(UnitSize, Count, Rest1) of
131+
{array, Type, List} ->
132+
Values = [{described, Descriptor, Value} || Value <- List],
133+
% this format cannot represent an empty array of described types
134+
{array, {described, Descriptor, Type}, Values};
135+
{as_is, _, _} ->
136+
exit({array_of_described_zero_width_elements_unsupported, Count})
137+
end;
138+
parse_array1(UnitSize, Count, <<Type, ArrayBin/binary>>)
139139
when Type >= 16#40 andalso Type =< 16#45 ->
140140
%% This is an array that must have zero octets of data.
141-
if byte_size(ArrayBin) > 0 ->
142-
exit({failed_to_parse_array_extra_input_remaining, Type, byte_size(ArrayBin)});
143-
Count > ?MAX_ZERO_WIDTH_ARRAY_COUNT ->
144-
exit({failed_to_parse_array_count_exceeds_limit, Type, Count});
145-
true ->
146-
{Value, _} = parse_array_primitive(Type, <<>>),
147-
{array, parse_constructor(Type), lists:duplicate(Count, Value)}
141+
case byte_size(ArrayBin) of
142+
0 ->
143+
%% "Count zero-width elements" costs a handful of bytes on the
144+
%% wire no matter how large Count is. Since RabbitMQ has no need to
145+
%% interpret this special type of array and to protect against CWE-770,
146+
%% instead of materialized as Count terms, keep it as an opaque,
147+
%% constant-size value.
148+
case UnitSize of
149+
8 -> {as_is, 16#e0, <<2:8, Count:8, Type>>};
150+
32 -> {as_is, 16#f0, <<5:32, Count:32, Type>>}
151+
end;
152+
Size ->
153+
exit({failed_to_parse_array_extra_input_remaining, Type, Size})
148154
end;
149-
parse_array1(Count, <<Type, ArrayBin/binary>>)
155+
parse_array1(_UnitSize, Count, <<Type, ArrayBin/binary>>)
150156
when Count > byte_size(ArrayBin) ->
151157
exit({failed_to_parse_array_count_exceeds_input, Type, Count, byte_size(ArrayBin)});
152-
parse_array1(Count, <<Type, ArrayBin/binary>>) ->
158+
parse_array1(_UnitSize, Count, <<Type, ArrayBin/binary>>) ->
153159
parse_array2(Count, Type, ArrayBin, []).
154160

155161
parse_array2(0, Type, <<>>, Acc) ->

deps/amqp10_common/test/binary_parser_SUITE.erl

Lines changed: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ all_tests() ->
2323
array_with_extra_input,
2424
array32_count_exceeds_data,
2525
array_of_zero_width_elements,
26+
array_of_described_zero_width_elements_unsupported,
2627
unsupported_type,
2728
peek_described_section_total_sizes,
2829
peek_described_section,
@@ -53,8 +54,10 @@ roundtrip(_Config) ->
5354
{binary, <<"outer value">>}},
5455
{array, ubyte, [{ubyte, 1}, {ubyte, 255}]},
5556
{array, boolean, [true, false, true]},
56-
{array, null, [null, null, null]},
57-
{array, null, []},
57+
%% Zero-width array elements (e.g. null repeated N times) are
58+
%% represented opaquely rather than expanded into N terms.
59+
{as_is, 16#f0, <<5:32, 3:32, 16#40>>},
60+
{as_is, 16#f0, <<5:32, 0:32, 16#40>>},
5861
true,
5962
{list, [{utf8, <<"hi">>},
6063
{described,
@@ -90,42 +93,72 @@ array_with_extra_input(_Config) ->
9093
?assertExit(Expected, amqp10_binary_parser:parse_many(Bin, [])).
9194

9295
array32_count_exceeds_data(_Config) ->
93-
Count = 16#FFFFFFFF,
94-
Type = 16#45,
95-
ArrayPayload = <<Count:32, Type>>,
96-
Size = byte_size(ArrayPayload),
97-
Bin = <<16#f0, Size:32, ArrayPayload/binary>>,
98-
?assertExit(
99-
{failed_to_parse_array_count_exceeds_limit, Type, Count},
100-
amqp10_binary_parser:parse(Bin)),
101-
%% Also verify smaller mismatches are caught: 10 elements but only 3 data bytes
102-
SmallType = 16#50,
103-
SmallPayload = <<10:32, SmallType, 1, 2, 3>>,
104-
SmallSize = byte_size(SmallPayload),
105-
SmallBin = <<16#f0, SmallSize:32, SmallPayload/binary>>,
106-
?assertExit(
107-
{failed_to_parse_array_count_exceeds_input, SmallType, 10, 3},
108-
amqp10_binary_parser:parse(SmallBin)).
109-
96+
TypeArray32 = 16#f0,
97+
TypeUByte = 16#50,
98+
%% 10 elements declared but only 3 data bytes available.
99+
Count = 10,
100+
Payload = <<Count:32, TypeUByte, 1, 2, 3>>,
101+
Bin = <<TypeArray32, (byte_size(Payload)):32, Payload/binary>>,
102+
?assertExit({failed_to_parse_array_count_exceeds_input, TypeUByte, Count, 3},
103+
amqp10_binary_parser:parse(Bin)).
104+
105+
%% Zero-width array elements (null, booleans, uint0, ulong0, list0) cost zero
106+
%% octets on the wire no matter how large Count is. Rather than materializing
107+
%% Count terms (CWE-770: a small frame could otherwise amplify into gigabytes
108+
%% of heap), the parser represents such an array as an opaque, constant-size value.
110109
array_of_zero_width_elements(_Config) ->
111-
%% Array8 (0xe0), Size=2, Count=10, Null Constructor=0x40 (width 0)
112-
{Parsed40, 4} = amqp10_binary_parser:parse(<<16#e0, 2, 10, 16#40>>),
113-
?assertEqual({array, null, lists:duplicate(10, null)}, Parsed40),
114-
115-
{Parsed41, 4} = amqp10_binary_parser:parse(<<16#e0, 2, 10, 16#41>>),
116-
?assertEqual({array, boolean, lists:duplicate(10, true)}, Parsed41),
117-
118-
{Parsed42, 4} = amqp10_binary_parser:parse(<<16#e0, 2, 10, 16#42>>),
119-
?assertEqual({array, boolean, lists:duplicate(10, false)}, Parsed42),
120-
121-
{Parsed43, 4} = amqp10_binary_parser:parse(<<16#e0, 2, 10, 16#43>>),
122-
?assertEqual({array, uint, lists:duplicate(10, {uint, 0})}, Parsed43),
123-
124-
{Parsed44, 4} = amqp10_binary_parser:parse(<<16#e0, 2, 10, 16#44>>),
125-
?assertEqual({array, ulong, lists:duplicate(10, {ulong, 0})}, Parsed44),
126-
127-
{Parsed45, 4} = amqp10_binary_parser:parse(<<16#e0, 2, 10, 16#45>>),
128-
?assertEqual({array, list, lists:duplicate(10, {list, []})}, Parsed45).
110+
TypeArray32 = 16#f0,
111+
TypeArray8 = 16#e0,
112+
113+
Check32 = fun(Type, Count) ->
114+
Encoded = <<5:32, Count:32, Type>>,
115+
Bin = <<TypeArray32, Encoded/binary>>,
116+
{Parsed, ParsedSize} = amqp10_binary_parser:parse(Bin),
117+
?assertEqual({as_is, TypeArray32, Encoded}, Parsed),
118+
?assertEqual(byte_size(Bin), ParsedSize),
119+
%% Re-encoding reproduces the exact original bytes.
120+
?assertEqual(Bin, iolist_to_binary(amqp10_binary_generator:generate(Parsed)))
121+
end,
122+
[Check32(Type, 10) || Type <- [16#40, 16#41, 16#42, 16#43, 16#44, 16#45]],
123+
124+
%% No limit on Count: an absurd 32-bit count is represented exactly as cheaply as a small one.
125+
Check32(16#40, 16#FFFFFFFF),
126+
127+
%% Array8 form (0xe0) is parsed just as cheaply, and the selector is
128+
%% preserved rather than normalized to array32: re-encoding reproduces
129+
%% the exact original 4 bytes, not an inflated 10-byte array32 form.
130+
Check8 = fun(Type, Count) ->
131+
Encoded = <<2, Count, Type>>,
132+
Bin = <<TypeArray8, Encoded/binary>>,
133+
{Parsed, ParsedSize} = amqp10_binary_parser:parse(Bin),
134+
?assertEqual({as_is, TypeArray8, Encoded}, Parsed),
135+
?assertEqual(byte_size(Bin), ParsedSize),
136+
?assertEqual(Bin, iolist_to_binary(amqp10_binary_generator:generate(Parsed)))
137+
end,
138+
[Check8(Type, 10) || Type <- [16#40, 16#41, 16#42, 16#43, 16#44, 16#45]],
139+
Check8(16#40, 255),
140+
141+
%% Nested as an element of an outer array of arrays, the preserved array8
142+
%% selector must be upgraded to array32 framing: RabbitMQ's generator
143+
%% always uses the large (32-bit) form for nested array elements (see
144+
%% constructor/1: "use large array type for all nested arrays").
145+
{Inner8, _} = amqp10_binary_parser:parse(<<TypeArray8, 2:8, 10:8, 16#40>>),
146+
OuterBin = iolist_to_binary(amqp10_binary_generator:generate({array, array, [Inner8]})),
147+
{{array, array, [InnerParsedBack]}, _} = amqp10_binary_parser:parse(OuterBin),
148+
?assertEqual({as_is, TypeArray32, <<5:32, 10:32, 16#40>>}, InnerParsedBack).
149+
150+
%% An array of described zero-width elements (e.g. a described null repeated
151+
%% Count times) carries no information beyond its count and descriptor. This
152+
%% combination isn't supported: it is rejected cleanly instead of being
153+
%% expanded (unbounded memory) or given a bespoke opaque representation.
154+
array_of_described_zero_width_elements_unsupported(_Config) ->
155+
%% ?DESCRIBED, utf8 "URL" descriptor, null (zero-width) element type.
156+
DescribedNull = <<0, 16#a1, 3, "URL", 16#40>>,
157+
Count = 16#FFFFFFFF,
158+
CountAndV = <<Count:32, DescribedNull/binary>>,
159+
Bin = <<16#f0, (byte_size(CountAndV)):32, CountAndV/binary>>,
160+
?assertExit({array_of_described_zero_width_elements_unsupported, Count},
161+
amqp10_binary_parser:parse(Bin)).
129162

130163
unsupported_type(_Config) ->
131164
UnsupportedType = 16#02,

deps/amqp10_common/test/prop_SUITE.erl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,10 @@ fields() ->
336336
)}.
337337

338338
amqp_array() ->
339-
Gens = fixed_and_variable_width_types(),
339+
%% Excludes amqp_null(): an array of null is encoded with the zero-width
340+
%% null constructor (0x40), which the parser represents opaquely as
341+
%% {as_is, ...} rather than as {array, null, List}.
342+
Gens = fixed_and_variable_width_types() -- [amqp_null()],
340343
?LET(N,
341344
integer(1, length(Gens)),
342345
begin

deps/rabbit/test/amqp_client_SUITE.erl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7194,12 +7194,16 @@ decimal_types(Config) ->
71947194
Decimal128Zero = <<16#22, 16#08, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>,
71957195
Decimal3242 = <<16#22, 16#50, 16#00, 16#2A>>, % 42
71967196
Decimal32NaN = <<16#7C, 0, 0, 0>>,
7197+
%% array8 of 5 nulls: size=2, count=5, null element constructor (0x40).
7198+
NullArray5 = <<2, 5, 16#40>>,
71977199
Body = #'v1_0.amqp_value'{content = {list, [{as_is, 16#74, Decimal32Zero},
71987200
{as_is, 16#84, Decimal64Zero},
7199-
{as_is, 16#94, Decimal128Zero}]}},
7201+
{as_is, 16#94, Decimal128Zero},
7202+
{as_is, 16#e0, NullArray5}]}},
72007203
MsgAnns = #{<<"x-decimal-32">> => {as_is, 16#74, Decimal3242},
72017204
<<"x-decimal-64">> => {as_is, 16#84, Decimal64Zero},
72027205
<<"x-decimal-128">> => {as_is, 16#94, Decimal128Zero},
7206+
<<"x-null-array">> => {as_is, 16#e0, NullArray5},
72037207
<<"x-list">> => {list, [{as_is, 16#94, Decimal128Zero}]},
72047208
<<"x-map">> => {map, [{{utf8, <<"key-1">>},
72057209
{as_is, 16#94, Decimal128Zero}}]}},
@@ -7223,6 +7227,7 @@ decimal_types(Config) ->
72237227
?assertMatch(#{<<"x-decimal-32">> := {as_is, 16#74, Decimal3242},
72247228
<<"x-decimal-64">> := {as_is, 16#84, Decimal64Zero},
72257229
<<"x-decimal-128">> := {as_is, 16#94, Decimal128Zero},
7230+
<<"x-null-array">> := {as_is, 16#e0, NullArray5},
72267231
<<"x-list">> := [{as_is, 16#94, Decimal128Zero}],
72277232
<<"x-map">> := [{{utf8, <<"key-1">>},
72287233
{as_is, 16#94, Decimal128Zero}}]},

0 commit comments

Comments
 (0)