[Spice-devel] [PATCH spice-common 1/3] Fix integer overflows computing sizes

Frediano Ziglio fziglio at redhat.com
Thu Mar 22 11:02:27 UTC 2018


> 
> > On 21 Mar 2018, at 13:03, Frediano Ziglio <fziglio at redhat.com> wrote:
> > 
> >>> On 20 Mar 2018, at 11:41, Frediano Ziglio <fziglio at redhat.com> wrote:
> >>> 
> >>>>> 
> >>>>> On 19 Mar 2018, at 11:06, Frediano Ziglio <fziglio at redhat.com> wrote:
> >>>>> 
> >>>>> Make code safe using both 32 and 64 bit machine.
> >>>>> Consider that this code can be compiled for machines with 32 bit.
> >>>>> There are some arrays length which are 32 bit.
> >>>>> 
> >>>>> If size_t this can cause easily an overflow. For instance message_len
> >>>>> sending SPICE_MSG_NOTIFY messages are 32 bit and code add a small
> >>>>> constant (currently 24) before doing the test for size. Now passing
> >>>>> (uint32_t) -20 as message_len would lead to a size of 4 after the
> >>>>> addition. This overflow does not happen on 64 bit machine as the length
> >>>>> is converted to size_t.
> >>>> 
> >>>> Why not use size_t instead of uint64_t then?
> >>>> 
> >>> 
> >>> A multiplication between 32 bit integer and a 32 bit integer can
> >>> cause overflow if the result is a 32 bit. Using a 64 bit integer
> >>> to multiply a 32 bit integer by a 32 bit integer avoids the overflow.
> >>> On 32 bit systems usually size_t is a 32 bit.
> >> 
> >> You totally missed my point then.
> >> 
> > 
> > Well, from "Why not use size_t instead of uint64_t then?" is quite
> > hard to understand.
> 
> The question was “why not use size_t”. I was expecting you to think long and
> hard about why you could not use size_t, not to explain to me the
> differences between 32-bit and 64-bit machines :-)
> 
> The reason I asked this specific question is that if you have to do size
> computations using something else than size_t, its most often a sign that
> something is wrong. So you have to justify it. You will find below the
> detailed analysis that I expected you to do in response to my question (in
> particular after the “frob” hints).
> 
> > 
> >> The problem you identified with overflow is real, and must be fixed. But
> >> it’s
> >> an input validation problem, not an arithmetic problem.
> >> 
> >> Consider:
> >> 
> >> 	void *allocate_frobs(uint32_t n)
> >> 	{
> >> 		if (n > MAX_FROBS)	// input validation
> >> 			return NULL;
> >> 		size_t size = n * sizeof(frob) + sizeof(header);
> >> 		return malloc(size);
> >> 	}
> >> 
> >> This is better than:
> >> 
> >> 	void *allocate_frobs(uint32_t n)
> >> 	{
> >> 		uint64_t size = n * sizeof(frob) + sizeof(header);
> >> 		if (size > UINT32_MAX)
> >> 			return NULL;
> >> 		return malloc(size);
> >> 	}
> >> 
> >> And if you had the problem with a uint64_t as input, you would certainly
> >> not
> >> “fix” it with
> >> 
> >> 	void *allocate_frobs(uint64_t n)
> >> 	{
> >> 		double_or_uint128_t size = n * sizeof(frob) + sizeof(header);
> >> 		if (size > UINT64_MAX)
> >> 			return NULL;
> >> 		return malloc(size);
> >> 	}
> >> 
> > 
> > There's no such case in our protocol. Not sure if this is checked by the
> > Python code (I think it should).
> 
> It’s not an exact match, but it’s really close.
> 
> If you look at the generated code in generated_clients_demarshallers.c, the
> first function where your changes really matter is parse_msg_notify, where
> there is a 32-bit length value being read. It looks like this (removing
> irrelevant code):
> 
> static uint8_t * parse_msg_notify(uint8_t *message_start, uint8_t
> *message_end, SPICE_GNUC_UNUSED int minor, size_t *size,
> message_destructor_t *free_message)
> {
> #ifndef FREDIANO
>     size_t nw_size;
>     size_t mem_size;
>     size_t message__nw_size, message__mem_size;
>     uint32_t message__nelements;
> #else /* FREDIANO */
>     uint64_t nw_size;
>     uint64_t mem_size;
>     uint64_t message__nw_size, message__mem_size;
>     uint64_t message__nelements;
> #endif /* FREDIANO */
> 
>     { /* message */
>         uint32_t message_len__value;
>         message_len__value = read_uint32(pos);
>         message__nelements = message_len__value;
> 
>         message__nw_size = message__nelements;
>         message__mem_size = sizeof(uint8_t) * message__nelements;
>     }
> 
>     nw_size = 24 + message__nw_size;
>     mem_size = sizeof(SpiceMsgNotify) + message__mem_size;
> 
>     /* Check if message fits in reported side */
> #ifndef FREDIANO
>     if (start + nw_size > message_end) {
> #else /* FREDIANO */
>     if (nw_size > (uintptr_t) (message_end - start)) {
> #endif /* FREDIANO */
>         return NULL;
>     }
> 
>     /* Validated extents and calculated size */
> #ifndef FREDIANO
>     data = (uint8_t *)malloc(mem_size);
> #else /* FREDIANO */
>     data = (uint8_t *)(mem_size > UINT32_MAX ? NULL : malloc(mem_size));
> #endif /* FREDIANO */
> }
> 
> So we have the core elements of my short “frob” examples, i.e. a size
> computation with a multiplication, an input validation that already exists,
> and then a malloc. It’s really not that different from my frob example, only
> longer.
> 
> What I am pointing out is that the overflow in that case happens because of a
> missing input validation in the part commented as /* message */. This is
> where you need an additional input validation, so that you have changes that
> look more like:
> 
>     size_t nw_size;
>     size_t mem_size;
>     size_t message__nw_size, message__mem_size;
>     uint32_t message__nelements;
> 
>     { /* message */
>         uint32_t message_len__value;
>         message_len__value = read_uint32(pos);
>         message__nelements = message_len__value;
> #ifdef C3D
>         if (SPICE_UNLIKELY(message__nelements) > SPICE_OVERFLOW_PROTECTION) {
>             goto error;
>         }
> #endif
> 
>         message__nw_size = message__nelements;
>         message__mem_size = sizeof(uint8_t) * message__nelements;
>     }
> 
>     nw_size = 24 + message__nw_size;
>     mem_size = sizeof(SpiceMsgNotify) + message__mem_size;
> 
>     /* Check if message fits in reported side */
>     if (start + nw_size > message_end) {
>         return NULL;
>     }
> 
>     data = (uint8_t *)malloc(mem_size);
> }
> 
> where you set SPICE_OVERFLOW_PROTECTION to be small enough to detect invalid
> messages and avoid any overflow during the following computations.
> 
> You could generate it automatically, e.g. by checking what you multiply by
> and dividing UINT32_MAX by that. However, I would strongly advise against
> it. I would much rather have a specific “max” value for SPICE that not only
> blocks any risk of buffer overrun, but also detects “smaller” message
> corruptions. You don’t need your message len to be 0x80000002 to know that
> the message is bogus. Having 10MB in a “notify” message is already quite
> suspect, and letting values higher larger than that pass through could
> easily cause a 32-bit machine to run out of memory or run into other kinds
> of DOS-style behaviors.
> 
> > 
> >> Once you do the input validation on the incoming uint32_t, the rest of the
> >> computations should be done as size_t, i.e. 32-bit on a 32-bit machine.
> >> 
> > 
> > The problem is that there's no MAX_FROBS or better MAX_FROBS == UINT32_MAX.
> 
> So let’s invent one. It’s a one-line addition of a constant to the SPICE
> protocol. If you are wary of changing the protocol, you could simply
> document that as an implementation-defined limit.
> 
> > The protocol mandate that the array is contained in the message so at the
> > end
> > this patch make sure this not requiring additional protocol specification
> > and
> > the behaviour is coherent between 32 and 64 bit machines.
> 
> I believe that the additional protocol specification, e.g. stating that no
> message has more than 10M elements, would be worth the additional protection
> (not to mention not having to force 32-bit machines to do more expensive
> 64-bit computations). But as I said, if you don’t want this, you can simply
> make it an implementation limit. Actually, you could make that limit
> dependent on whether the machine is 32-bit or 64-bit.
> 

No problem but I have no time to do it now, the patch was written more than
2 years and a half ago to fix a security issue that got a CVE, so feel
free to provide patches.

Frediano

> > 
> >>> 
> >>>>> 
> >>>>> There are also some array length where some item are bigger than 1
> >>>>> byte.
> >>>>> For instance SPICE_MAIN_CHANNELS_LIST message have a number of channels
> >>>>> and each channel is composed by 2 bytes. Now the code generated try to
> >>>>> do
> >>>>> length * 2 where length is still a 32 bit so if we put a value like
> >>>>> 0x80000002u we get 4 as length. This will cause an overflow as code
> >>>>> will
> >>>>> allocate very few bytes but try to fill with a huge number of elements.
> >>>>> This overflow happen in both 32 and 64 bit machine.
> >>>>> 
> >>>>> To avoid all these possible overflows this patch use only 64 bit for
> >>>>> nelements (number of elements), nw_size (network size) and mem_size
> >>>>> (memory size needed) checking the sizes to avoid other overflows
> >>>>> (like pointers conversions under 32 bit machines).
> >>>>> 
> >>>>> Signed-off-by: Frediano Ziglio <fziglio at redhat.com>
> >>>>> ---
> >>>>> python_modules/demarshal.py | 38 +++++++++++++++++++-------------------
> >>>>> 1 file changed, 19 insertions(+), 19 deletions(-)
> >>>>> 
> >>>>> diff --git a/python_modules/demarshal.py b/python_modules/demarshal.py
> >>>>> index da87d44..7e73985 100644
> >>>>> --- a/python_modules/demarshal.py
> >>>>> +++ b/python_modules/demarshal.py
> >>>>> @@ -101,7 +101,7 @@ def write_parser_helpers(writer):
> >>>>>   writer.variable_def("uint64_t", "offset")
> >>>>>   writer.variable_def("parse_func_t", "parse")
> >>>>>   writer.variable_def("void **", "dest")
> >>>>> -    writer.variable_def("uint32_t", "nelements")
> >>>>> +    writer.variable_def("uint64_t", "nelements")
> >>>>>   writer.end_block(semicolon=True)
> >>>>> 
> >>>>> def write_read_primitive(writer, start, container, name, scope):
> >>>>> @@ -186,7 +186,7 @@ def write_validate_switch_member(writer, mprefix,
> >>>>> container, switch_member, scop
> >>>>> 
> >>>>>           all_as_extra_size = m.is_extra_size() and want_extra_size
> >>>>>           if not want_mem_size and all_as_extra_size and not
> >>>>>           scope.variable_defined(item.mem_size()):
> >>>>> -                scope.variable_def("uint32_t", item.mem_size())
> >>>>> +                scope.variable_def("uint64_t", item.mem_size())
> >>>>> 
> >>>>>           sub_want_mem_size = want_mem_size or all_as_extra_size
> >>>>>           sub_want_extra_size = want_extra_size and not
> >>>>>           all_as_extra_size
> >>>>> @@ -219,7 +219,7 @@ def write_validate_struct_function(writer, struct):
> >>>>>   scope = writer.function(validate_function, "static intptr_t",
> >>>>>   "uint8_t
> >>>>>   *message_start, uint8_t *message_end, uint64_t offset,
> >>>>>   SPICE_GNUC_UNUSED int minor")
> >>>>>   scope.variable_def("uint8_t *", "start = message_start + offset")
> >>>>>   scope.variable_def("SPICE_GNUC_UNUSED uint8_t *", "pos")
> >>>>> -    scope.variable_def("size_t", "mem_size", "nw_size")
> >>>>> +    scope.variable_def("uint64_t", "mem_size", "nw_size")
> >>>>>   num_pointers = struct.get_num_pointers()
> >>>>>   if  num_pointers != 0:
> >>>>>       scope.variable_def("SPICE_GNUC_UNUSED intptr_t", "ptr_size")
> >>>>> @@ -236,7 +236,7 @@ def write_validate_struct_function(writer, struct):
> >>>>> 
> >>>>>   writer.newline()
> >>>>>   writer.comment("Check if struct fits in reported side").newline()
> >>>>> -    writer.error_check("start + nw_size > message_end")
> >>>>> +    writer.error_check("nw_size > (uintptr_t) (message_end - start)")
> >>>>> 
> >>>>>   writer.statement("return mem_size")
> >>>>> 
> >>>>> @@ -264,26 +264,26 @@ def write_validate_pointer_item(writer,
> >>>>> container,
> >>>>> item, scope, parent_scope, st
> >>>>>       # if array, need no function check
> >>>>> 
> >>>>>       if target_type.is_array():
> >>>>> -            writer.error_check("message_start + %s >= message_end" %
> >>>>> v)
> >>>>> +            writer.error_check("%s >= (uintptr_t) (message_end -
> >>>>> message_start)" % v)
> >>>>> 
> >>>>> 
> >>>>>           assert target_type.element_type.is_primitive()
> >>>>> 
> >>>>>           array_item = ItemInfo(target_type, "%s__array" % item.prefix,
> >>>>>           start)
> >>>>> -            scope.variable_def("uint32_t", array_item.nw_size())
> >>>>> +            scope.variable_def("uint64_t", array_item.nw_size())
> >>>>>           # don't create a variable that isn't used, fixes
> >>>>>           -Werror=unused-but-set-variable
> >>>>>           need_mem_size = want_mem_size or (
> >>>>>               want_extra_size and not item.member.has_attr("chunk")
> >>>>>               and not target_type.is_cstring_length())
> >>>>>           if need_mem_size:
> >>>>> -                scope.variable_def("uint32_t", array_item.mem_size())
> >>>>> +                scope.variable_def("uint64_t", array_item.mem_size())
> >>>>>           if target_type.is_cstring_length():
> >>>>>               writer.assign(array_item.nw_size(), "spice_strnlen((char
> >>>>>               *)message_start + %s, message_end - (message_start +
> >>>>>               %s))"
> >>>>>               % (v, v))
> >>>>>               writer.error_check("*(message_start + %s + %s) != 0" %
> >>>>>               (v,
> >>>>>               array_item.nw_size()))
> >>>>>           else:
> >>>>>               write_validate_array_item(writer, container, array_item,
> >>>>>               scope, parent_scope, start,
> >>>>>                                         True,
> >>>>>                                         want_mem_size=need_mem_size,
> >>>>>                                         want_extra_size=False)
> >>>>> -                writer.error_check("message_start + %s + %s >
> >>>>> message_end"
> >>>>> % (v, array_item.nw_size()))
> >>>>> +                writer.error_check("%s + %s > (uintptr_t) (message_end
> >>>>> -
> >>>>> message_start)" % (v, array_item.nw_size()))
> >>>>> 
> >>>>>           if want_extra_size:
> >>>>>               if item.member and item.member.has_attr("chunk"):
> >>>>> @@ -321,11 +321,11 @@ def write_validate_array_item(writer, container,
> >>>>> item, scope, parent_scope, star
> >>>>>       nelements = "%s__nbytes" %(item.prefix)
> >>>>>       real_nelements = "%s__nelements" %(item.prefix)
> >>>>>       if not parent_scope.variable_defined(real_nelements):
> >>>>> -            parent_scope.variable_def("uint32_t", real_nelements)
> >>>>> +            parent_scope.variable_def("uint64_t", real_nelements)
> >>>>>   else:
> >>>>>       nelements = "%s__nelements" %(item.prefix)
> >>>>>   if not parent_scope.variable_defined(nelements):
> >>>>> -        parent_scope.variable_def("uint32_t", nelements)
> >>>>> +        parent_scope.variable_def("uint64_t", nelements)
> >>>>> 
> >>>>>   if array.is_constant_length():
> >>>>>       writer.assign(nelements, array.size)
> >>>>> @@ -420,10 +420,10 @@ def write_validate_array_item(writer, container,
> >>>>> item, scope, parent_scope, star
> >>>>>   element_nw_size = element_item.nw_size()
> >>>>>   element_mem_size = element_item.mem_size()
> >>>>>   element_extra_size = element_item.extra_size()
> >>>>> -    scope.variable_def("uint32_t", element_nw_size)
> >>>>> -    scope.variable_def("uint32_t", element_mem_size)
> >>>>> +    scope.variable_def("uint64_t", element_nw_size)
> >>>>> +    scope.variable_def("uint64_t", element_mem_size)
> >>>>>   if want_extra_size:
> >>>>> -        scope.variable_def("uint32_t", element_extra_size)
> >>>>> +        scope.variable_def("uint64_t", element_extra_size)
> >>>>> 
> >>>>>   if want_nw_size:
> >>>>>       writer.assign(nw_size, 0)
> >>>>> @@ -556,7 +556,7 @@ def write_validate_container(writer, prefix,
> >>>>> container,
> >>>>> start, parent_scope, wan
> >>>>>       sub_want_nw_size = want_nw_size and not m.is_fixed_nw_size()
> >>>>>       sub_want_mem_size = m.is_extra_size() and want_mem_size
> >>>>>       sub_want_extra_size = not m.is_extra_size() and
> >>>>>       m.contains_extra_size()
> >>>>> -        defs = ["size_t"]
> >>>>> +        defs = ["uint64_t"]
> >>>>>       name = prefix_m(prefix, m)
> >>>>>       if sub_want_nw_size:
> >>>>> 
> >>>>> @@ -697,7 +697,7 @@ def read_array_len(writer, prefix, array, dest,
> >>>>> scope,
> >>>>> is_ptr):
> >>>>>   if dest.is_toplevel() and scope.variable_defined(nelements):
> >>>>>       return nelements # Already there for toplevel, need not
> >>>>>       recalculate
> >>>>>   element_type = array.element_type
> >>>>> -    scope.variable_def("uint32_t", nelements)
> >>>>> +    scope.variable_def("uint64_t", nelements)
> >>>>>   if array.is_constant_length():
> >>>>>       writer.assign(nelements, array.size)
> >>>>>   elif array.is_identifier_length():
> >>>>> @@ -1053,9 +1053,9 @@ def write_msg_parser(writer, message):
> >>>>>   parent_scope.variable_def("SPICE_GNUC_UNUSED uint8_t *", "pos")
> >>>>>   parent_scope.variable_def("uint8_t *", "start = message_start")
> >>>>>   parent_scope.variable_def("uint8_t *", "data = NULL")
> >>>>> -    parent_scope.variable_def("size_t", "nw_size")
> >>>>> +    parent_scope.variable_def("uint64_t", "nw_size")
> >>>>>   if want_mem_size:
> >>>>> -        parent_scope.variable_def("size_t", "mem_size")
> >>>>> +        parent_scope.variable_def("uint64_t", "mem_size")
> >>>>>   if not message.has_attr("nocopy"):
> >>>>>       parent_scope.variable_def("uint8_t *", "in", "end")
> >>>>>   num_pointers = message.get_num_pointers()
> >>>>> @@ -1073,7 +1073,7 @@ def write_msg_parser(writer, message):
> >>>>>   writer.newline()
> >>>>> 
> >>>>>   writer.comment("Check if message fits in reported side").newline()
> >>>>> -    with writer.block("if (start + nw_size > message_end)"):
> >>>>> +    with writer.block("if (nw_size > (uintptr_t) (message_end -
> >>>>> start))"):
> >>>>>       writer.statement("return NULL")
> >>>>> 
> >>>>>   writer.newline().comment("Validated extents and calculated
> >>>>>   size").newline()
> >>>>> @@ -1084,7 +1084,7 @@ def write_msg_parser(writer, message):
> >>>>>       writer.assign("*size", "message_end - message_start")
> >>>>>       writer.assign("*free_message", "nofree")
> >>>>>   else:
> >>>>> -        writer.assign("data", "(uint8_t *)malloc(mem_size)")
> >>>>> +        writer.assign("data", "(uint8_t *)(mem_size > UINT32_MAX ?
> >>>>> NULL
> >>>>> :
> >>>>> malloc(mem_size))")
> >>>>>       writer.error_check("data == NULL")
> >>>>>       writer.assign("end", "data + %s" % (msg_sizeof))
> >>>>>       writer.assign("in", "start").newline()


More information about the Spice-devel mailing list