From 1deacdd4e8e35a5cf1417918ca4f6b0afa6409b1 Mon Sep 17 00:00:00 2001 From: William Manley Date: Tue, 23 Jun 2020 22:59:58 +0100 Subject: [PATCH 01/18] gvariant-core: Consolidate construction of `GVariantSerialised` So I only need to change it in one place. This introduces no functional changes. Helps: #2121 --- glib/gvariant-core.c | 49 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/glib/gvariant-core.c b/glib/gvariant-core.c index 89ea54c013..d5d78da88b 100644 --- a/glib/gvariant-core.c +++ b/glib/gvariant-core.c @@ -351,6 +351,27 @@ g_variant_ensure_size (GVariant *value) } } +/* < private > + * g_variant_to_serialised: + * @value: a #GVariant + * + * Gets a GVariantSerialised for a GVariant in state STATE_SERIALISED. + */ +inline static GVariantSerialised +g_variant_to_serialised (GVariant *value) +{ + g_assert (value->state & STATE_SERIALISED); + { + GVariantSerialised serialised = { + value->type_info, + (gpointer) value->contents.serialised.data, + value->size, + value->depth, + }; + return serialised; + } +} + /* < private > * g_variant_serialise: * @value: a #GVariant @@ -1009,16 +1030,8 @@ g_variant_n_children (GVariant *value) g_variant_lock (value); if (value->state & STATE_SERIALISED) - { - GVariantSerialised serialised = { - value->type_info, - (gpointer) value->contents.serialised.data, - value->size, - value->depth, - }; - - n_children = g_variant_serialised_n_children (serialised); - } + n_children = g_variant_serialised_n_children ( + g_variant_to_serialised (value)); else n_children = value->contents.tree.n_children; @@ -1085,12 +1098,7 @@ g_variant_get_child_value (GVariant *value, } { - GVariantSerialised serialised = { - value->type_info, - (gpointer) value->contents.serialised.data, - value->size, - value->depth, - }; + GVariantSerialised serialised = g_variant_to_serialised (value); GVariantSerialised s_child; GVariant *child; @@ -1203,14 +1211,7 @@ g_variant_is_normal_form (GVariant *value) if (value->state & STATE_SERIALISED) { - GVariantSerialised serialised = { - value->type_info, - (gpointer) value->contents.serialised.data, - value->size, - value->depth - }; - - if (g_variant_serialised_is_normal (serialised)) + if (g_variant_serialised_is_normal (g_variant_to_serialised (value))) value->state |= STATE_TRUSTED; } else -- GitLab From 446e69f5edd72deb2196dee36bbaf8056caf6948 Mon Sep 17 00:00:00 2001 From: William Manley Date: Thu, 25 Jun 2020 17:08:21 +0100 Subject: [PATCH 02/18] gvariant-serialiser: Factor out functions for dealing with framing offsets This introduces no functional changes. Helps: #2121 --- glib/gvariant-serialiser.c | 108 +++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/glib/gvariant-serialiser.c b/glib/gvariant-serialiser.c index 6ebaec7d40..1eaa80b29f 100644 --- a/glib/gvariant-serialiser.c +++ b/glib/gvariant-serialiser.c @@ -635,30 +635,62 @@ gvs_calculate_total_size (gsize body_size, return body_size + 8 * offsets; } +struct Offsets +{ + gsize data_size; + + guchar *array; + gsize length; + guint offset_size; + + gboolean is_normal; +}; + static gsize -gvs_variable_sized_array_n_children (GVariantSerialised value) +gvs_offsets_get_offset_n (struct Offsets *offsets, + gsize n) +{ + return gvs_read_unaligned_le ( + offsets->array + (offsets->offset_size * n), offsets->offset_size); +} + +static struct Offsets +gvs_variable_sized_array_get_frame_offsets (GVariantSerialised value) { + struct Offsets out = { 0, }; gsize offsets_array_size; - gsize offset_size; gsize last_end; if (value.size == 0) - return 0; - - offset_size = gvs_get_offset_size (value.size); + { + out.is_normal = TRUE; + return out; + } - last_end = gvs_read_unaligned_le (value.data + value.size - - offset_size, offset_size); + out.offset_size = gvs_get_offset_size (value.size); + last_end = gvs_read_unaligned_le (value.data + value.size - out.offset_size, + out.offset_size); if (last_end > value.size) - return 0; + return out; /* offsets not normal */ offsets_array_size = value.size - last_end; - if (offsets_array_size % offset_size) - return 0; + if (offsets_array_size % out.offset_size) + return out; /* offsets not normal */ + + out.data_size = last_end; + out.array = value.data + last_end; + out.length = offsets_array_size / out.offset_size; + out.is_normal = TRUE; - return offsets_array_size / offset_size; + return out; +} + +static gsize +gvs_variable_sized_array_n_children (GVariantSerialised value) +{ + return gvs_variable_sized_array_get_frame_offsets (value).length; } static GVariantSerialised @@ -666,8 +698,9 @@ gvs_variable_sized_array_get_child (GVariantSerialised value, gsize index_) { GVariantSerialised child = { 0, }; - gsize offset_size; - gsize last_end; + + struct Offsets offsets = gvs_variable_sized_array_get_frame_offsets (value); + gsize start; gsize end; @@ -675,18 +708,11 @@ gvs_variable_sized_array_get_child (GVariantSerialised value, g_variant_type_info_ref (child.type_info); child.depth = value.depth + 1; - offset_size = gvs_get_offset_size (value.size); - - last_end = gvs_read_unaligned_le (value.data + value.size - - offset_size, offset_size); - if (index_ > 0) { guint alignment; - start = gvs_read_unaligned_le (value.data + last_end + - (offset_size * (index_ - 1)), - offset_size); + start = gvs_offsets_get_offset_n (&offsets, index_ - 1); g_variant_type_info_query (child.type_info, &alignment, NULL); start += (-start) & alignment; @@ -694,11 +720,9 @@ gvs_variable_sized_array_get_child (GVariantSerialised value, else start = 0; - end = gvs_read_unaligned_le (value.data + last_end + - (offset_size * index_), - offset_size); + end = gvs_offsets_get_offset_n (&offsets, index_); - if (start < end && end <= value.size && end <= last_end) + if (start < end && end <= value.size && end <= offsets.data_size) { child.data = value.data + start; child.size = end - start; @@ -770,34 +794,16 @@ static gboolean gvs_variable_sized_array_is_normal (GVariantSerialised value) { GVariantSerialised child = { 0, }; - gsize offsets_array_size; - guchar *offsets_array; - guint offset_size; guint alignment; - gsize last_end; - gsize length; gsize offset; gsize i; - if (value.size == 0) - return TRUE; - - offset_size = gvs_get_offset_size (value.size); - last_end = gvs_read_unaligned_le (value.data + value.size - - offset_size, offset_size); + struct Offsets offsets = gvs_variable_sized_array_get_frame_offsets (value); - if (last_end > value.size) + if (!offsets.is_normal) return FALSE; - offsets_array_size = value.size - last_end; - - if (offsets_array_size % offset_size) - return FALSE; - - offsets_array = value.data + value.size - offsets_array_size; - length = offsets_array_size / offset_size; - - if (length == 0) + if (value.size != 0 && offsets.length == 0) return FALSE; child.type_info = g_variant_type_info_element (value.type_info); @@ -805,14 +811,14 @@ gvs_variable_sized_array_is_normal (GVariantSerialised value) child.depth = value.depth + 1; offset = 0; - for (i = 0; i < length; i++) + for (i = 0; i < offsets.length; i++) { gsize this_end; - this_end = gvs_read_unaligned_le (offsets_array + offset_size * i, - offset_size); + this_end = gvs_read_unaligned_le (offsets.array + offsets.offset_size * i, + offsets.offset_size); - if (this_end < offset || this_end > last_end) + if (this_end < offset || this_end > offsets.data_size) return FALSE; while (offset & alignment) @@ -834,7 +840,7 @@ gvs_variable_sized_array_is_normal (GVariantSerialised value) offset = this_end; } - g_assert (offset == last_end); + g_assert (offset == offsets.data_size); return TRUE; } -- GitLab From 298a537d5f6783e55d87e40011ee3fd3b22b72f9 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 25 Oct 2022 18:05:52 +0100 Subject: [PATCH 03/18] gvariant: Zero-initialise various GVariantSerialised objects The following few commits will add a couple of new fields to `GVariantSerialised`, and they should be zero-filled by default. Try and pre-empt that a bit by zero-filling `GVariantSerialised` by default in a few places. Signed-off-by: Philip Withnall Helps: #2121 --- glib/gvariant.c | 2 +- glib/tests/gvariant.c | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/glib/gvariant.c b/glib/gvariant.c index 6863f341d6..e4c85ca636 100644 --- a/glib/gvariant.c +++ b/glib/gvariant.c @@ -6002,7 +6002,7 @@ g_variant_byteswap (GVariant *value) if (alignment) /* (potentially) contains multi-byte numeric data */ { - GVariantSerialised serialised; + GVariantSerialised serialised = { 0, }; GVariant *trusted; GBytes *bytes; diff --git a/glib/tests/gvariant.c b/glib/tests/gvariant.c index 058d02ad22..18fa855aeb 100644 --- a/glib/tests/gvariant.c +++ b/glib/tests/gvariant.c @@ -1440,7 +1440,7 @@ test_maybe (void) for (flavour = 0; flavour < 8; flavour += alignment) { - GVariantSerialised serialised; + GVariantSerialised serialised = { 0, }; GVariantSerialised child; serialised.type_info = type_info; @@ -1564,7 +1564,7 @@ test_array (void) for (flavour = 0; flavour < 8; flavour += alignment) { - GVariantSerialised serialised; + GVariantSerialised serialised = { 0, }; serialised.type_info = array_info; serialised.data = flavoured_malloc (needed_size, flavour); @@ -1728,7 +1728,7 @@ test_tuple (void) for (flavour = 0; flavour < 8; flavour += alignment) { - GVariantSerialised serialised; + GVariantSerialised serialised = { 0, }; serialised.type_info = type_info; serialised.data = flavoured_malloc (needed_size, flavour); @@ -1823,7 +1823,7 @@ test_variant (void) for (flavour = 0; flavour < 8; flavour += alignment) { - GVariantSerialised serialised; + GVariantSerialised serialised = { 0, }; GVariantSerialised child; serialised.type_info = type_info; @@ -2270,7 +2270,7 @@ serialise_tree (TreeInstance *tree, static void test_byteswap (void) { - GVariantSerialised one, two; + GVariantSerialised one = { 0, }, two = { 0, }; TreeInstance *tree; tree = tree_instance_new (NULL, 3); @@ -2344,7 +2344,7 @@ test_serialiser_children (void) static void test_fuzz (gdouble *fuzziness) { - GVariantSerialised serialised; + GVariantSerialised serialised = { 0, }; TreeInstance *tree; /* make an instance */ -- GitLab From ade71fb544391b2e33e1859645726bfee0d5eaaf Mon Sep 17 00:00:00 2001 From: William Manley Date: Mon, 29 Jun 2020 16:59:44 +0100 Subject: [PATCH 04/18] =?UTF-8?q?gvariant:=20Don=E2=80=99t=20allow=20child?= =?UTF-8?q?=20elements=20to=20overlap=20with=20each=20other?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If different elements of a variable sized array can overlap with each other then we can cause a `GVariant` to normalise to a much larger type. This commit changes the behaviour of `GVariant` with non-normal form data. If an invalid frame offset is found all subsequent elements are given their default value. When retrieving an element at index `n` we scan the frame offsets up to index `n` and if they are not in order we return an element with the default value for that type. This guarantees that elements don't overlap with each other. We remember the offset we've scanned up to so we don't need to repeat this work on subsequent accesses. We skip these checks for trusted data. Unfortunately this makes random access of untrusted data O(n) — at least on first access. It doesn't affect the algorithmic complexity of accessing elements in order, such as when using the `GVariantIter` interface. Also: the cost of validation will be amortised as the `GVariant` instance is continued to be used. I've implemented this with 4 different functions, 1 for each element size, rather than looping calling `gvs_read_unaligned_le` in the hope that the compiler will find it easy to optimise and should produce fairly tight code. Fixes: #2121 --- glib/gvariant-core.c | 35 ++++++++++++++++ glib/gvariant-serialiser.c | 86 ++++++++++++++++++++++++++++++++++++-- glib/gvariant-serialiser.h | 9 ++++ glib/tests/gvariant.c | 45 ++++++++++++++++++++ 4 files changed, 172 insertions(+), 3 deletions(-) diff --git a/glib/gvariant-core.c b/glib/gvariant-core.c index d5d78da88b..f25d472794 100644 --- a/glib/gvariant-core.c +++ b/glib/gvariant-core.c @@ -67,6 +67,7 @@ struct _GVariant { GBytes *bytes; gconstpointer data; + gsize ordered_offsets_up_to; } serialised; struct @@ -164,6 +165,24 @@ struct _GVariant * if .data pointed to the appropriate number of nul * bytes. * + * .ordered_offsets_up_to: If ordered_offsets_up_to == n this means that all + * the frame offsets up to and including the frame + * offset determining the end of element n are in + * order. This guarantees that the bytes of element + * n don't overlap with any previous element. + * + * For trusted data this is set to G_MAXSIZE and we + * don't check that the frame offsets are in order. + * + * Note: This doesn't imply the offsets are good in + * any way apart from their ordering. In particular + * offsets may be out of bounds for this value or + * may imply that the data overlaps the frame + * offsets themselves. + * + * This field is only relevant for arrays of non + * fixed width types. + * * .tree: Only valid when the instance is in tree form. * * Note that accesses from other threads could result in @@ -367,6 +386,7 @@ g_variant_to_serialised (GVariant *value) (gpointer) value->contents.serialised.data, value->size, value->depth, + value->contents.serialised.ordered_offsets_up_to, }; return serialised; } @@ -398,6 +418,7 @@ g_variant_serialise (GVariant *value, serialised.size = value->size; serialised.data = data; serialised.depth = value->depth; + serialised.ordered_offsets_up_to = 0; children = (gpointer *) value->contents.tree.children; n_children = value->contents.tree.n_children; @@ -441,6 +462,15 @@ g_variant_fill_gvs (GVariantSerialised *serialised, g_assert (serialised->size == value->size); serialised->depth = value->depth; + if (value->state & STATE_SERIALISED) + { + serialised->ordered_offsets_up_to = value->contents.serialised.ordered_offsets_up_to; + } + else + { + serialised->ordered_offsets_up_to = 0; + } + if (serialised->data) /* g_variant_store() is a public API, so it * it will reacquire the lock if it needs to. @@ -483,6 +513,7 @@ g_variant_ensure_serialised (GVariant *value) bytes = g_bytes_new_take (data, value->size); value->contents.serialised.data = g_bytes_get_data (bytes, NULL); value->contents.serialised.bytes = bytes; + value->contents.serialised.ordered_offsets_up_to = G_MAXSIZE; value->state |= STATE_SERIALISED; } } @@ -563,6 +594,7 @@ g_variant_new_from_bytes (const GVariantType *type, serialised.type_info = value->type_info; serialised.data = (guchar *) g_bytes_get_data (bytes, &serialised.size); serialised.depth = 0; + serialised.ordered_offsets_up_to = trusted ? G_MAXSIZE : 0; if (!g_variant_serialised_check (serialised)) { @@ -613,6 +645,8 @@ g_variant_new_from_bytes (const GVariantType *type, value->contents.serialised.data = g_bytes_get_data (bytes, &value->size); } + value->contents.serialised.ordered_offsets_up_to = trusted ? G_MAXSIZE : 0; + g_clear_pointer (&owned_bytes, g_bytes_unref); return value; @@ -1132,6 +1166,7 @@ g_variant_get_child_value (GVariant *value, child->contents.serialised.bytes = g_bytes_ref (value->contents.serialised.bytes); child->contents.serialised.data = s_child.data; + child->contents.serialised.ordered_offsets_up_to = s_child.ordered_offsets_up_to; return child; } diff --git a/glib/gvariant-serialiser.c b/glib/gvariant-serialiser.c index 1eaa80b29f..c33a903bff 100644 --- a/glib/gvariant-serialiser.c +++ b/glib/gvariant-serialiser.c @@ -1,6 +1,7 @@ /* * Copyright © 2007, 2008 Ryan Lortie * Copyright © 2010 Codethink Limited + * Copyright © 2020 William Manley * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -266,6 +267,7 @@ gvs_fixed_sized_maybe_get_child (GVariantSerialised value, value.type_info = g_variant_type_info_element (value.type_info); g_variant_type_info_ref (value.type_info); value.depth++; + value.ordered_offsets_up_to = 0; return value; } @@ -297,7 +299,7 @@ gvs_fixed_sized_maybe_serialise (GVariantSerialised value, { if (n_children) { - GVariantSerialised child = { NULL, value.data, value.size, value.depth + 1 }; + GVariantSerialised child = { NULL, value.data, value.size, value.depth + 1, 0 }; gvs_filler (&child, children[0]); } @@ -319,6 +321,7 @@ gvs_fixed_sized_maybe_is_normal (GVariantSerialised value) /* proper element size: "Just". recurse to the child. */ value.type_info = g_variant_type_info_element (value.type_info); value.depth++; + value.ordered_offsets_up_to = 0; return g_variant_serialised_is_normal (value); } @@ -360,6 +363,7 @@ gvs_variable_sized_maybe_get_child (GVariantSerialised value, value.data = NULL; value.depth++; + value.ordered_offsets_up_to = 0; return value; } @@ -390,7 +394,7 @@ gvs_variable_sized_maybe_serialise (GVariantSerialised value, { if (n_children) { - GVariantSerialised child = { NULL, value.data, value.size - 1, value.depth + 1 }; + GVariantSerialised child = { NULL, value.data, value.size - 1, value.depth + 1, 0 }; /* write the data for the child. */ gvs_filler (&child, children[0]); @@ -410,6 +414,7 @@ gvs_variable_sized_maybe_is_normal (GVariantSerialised value) value.type_info = g_variant_type_info_element (value.type_info); value.size--; value.depth++; + value.ordered_offsets_up_to = 0; return g_variant_serialised_is_normal (value); } @@ -693,6 +698,32 @@ gvs_variable_sized_array_n_children (GVariantSerialised value) return gvs_variable_sized_array_get_frame_offsets (value).length; } +/* Find the index of the first out-of-order element in @data, assuming that + * @data is an array of elements of given @type, starting at index @start and + * containing a further @len-@start elements. */ +#define DEFINE_FIND_UNORDERED(type) \ + static gsize \ + find_unordered_##type (const guint8 *data, gsize start, gsize len) \ + { \ + gsize off; \ + type current, previous; \ + \ + memcpy (&previous, data + start * sizeof (current), sizeof (current)); \ + for (off = (start + 1) * sizeof (current); off < len * sizeof (current); off += sizeof (current)) \ + { \ + memcpy (¤t, data + off, sizeof (current)); \ + if (current < previous) \ + break; \ + previous = current; \ + } \ + return off / sizeof (current) - 1; \ + } + +DEFINE_FIND_UNORDERED (guint8); +DEFINE_FIND_UNORDERED (guint16); +DEFINE_FIND_UNORDERED (guint32); +DEFINE_FIND_UNORDERED (guint64); + static GVariantSerialised gvs_variable_sized_array_get_child (GVariantSerialised value, gsize index_) @@ -708,6 +739,49 @@ gvs_variable_sized_array_get_child (GVariantSerialised value, g_variant_type_info_ref (child.type_info); child.depth = value.depth + 1; + /* If the requested @index_ is beyond the set of indices whose framing offsets + * have been checked, check the remaining offsets to see whether they’re + * normal (in order, no overlapping array elements). */ + if (index_ > value.ordered_offsets_up_to) + { + switch (offsets.offset_size) + { + case 1: + { + value.ordered_offsets_up_to = find_unordered_guint8 ( + offsets.array, value.ordered_offsets_up_to, index_ + 1); + break; + } + case 2: + { + value.ordered_offsets_up_to = find_unordered_guint16 ( + offsets.array, value.ordered_offsets_up_to, index_ + 1); + break; + } + case 4: + { + value.ordered_offsets_up_to = find_unordered_guint32 ( + offsets.array, value.ordered_offsets_up_to, index_ + 1); + break; + } + case 8: + { + value.ordered_offsets_up_to = find_unordered_guint64 ( + offsets.array, value.ordered_offsets_up_to, index_ + 1); + break; + } + default: + /* gvs_get_offset_size() only returns maximum 8 */ + g_assert_not_reached (); + } + } + + if (index_ > value.ordered_offsets_up_to) + { + /* Offsets are invalid somewhere, so return an empty child. */ + return child; + } + if (index_ > 0) { guint alignment; @@ -842,6 +916,9 @@ gvs_variable_sized_array_is_normal (GVariantSerialised value) g_assert (offset == offsets.data_size); + /* All offsets have now been checked. */ + value.ordered_offsets_up_to = G_MAXSIZE; + return TRUE; } @@ -1078,7 +1155,7 @@ gvs_tuple_is_normal (GVariantSerialised value) for (i = 0; i < length; i++) { const GVariantMemberInfo *member_info; - GVariantSerialised child; + GVariantSerialised child = { 0, }; gsize fixed_size; guint alignment; gsize end; @@ -1138,6 +1215,9 @@ gvs_tuple_is_normal (GVariantSerialised value) offset = end; } + /* All element bounds have been checked above. */ + value.ordered_offsets_up_to = G_MAXSIZE; + { gsize fixed_size; guint alignment; diff --git a/glib/gvariant-serialiser.h b/glib/gvariant-serialiser.h index 6ced7e3d6c..a1bccb834b 100644 --- a/glib/gvariant-serialiser.h +++ b/glib/gvariant-serialiser.h @@ -31,6 +31,15 @@ typedef struct guchar *data; gsize size; gsize depth; /* same semantics as GVariant.depth */ + + /* If ordered_offsets_up_to == n this means that all the frame offsets up to and + * including the frame offset determining the end of element n are in order. + * This guarantees that the bytes of element n don't overlap with any previous + * element. + * + * This is both read and set by g_variant_serialised_get_child for arrays of + * non-fixed-width types */ + gsize ordered_offsets_up_to; } GVariantSerialised; /* deserialization */ diff --git a/glib/tests/gvariant.c b/glib/tests/gvariant.c index 18fa855aeb..45e302c926 100644 --- a/glib/tests/gvariant.c +++ b/glib/tests/gvariant.c @@ -1,5 +1,6 @@ /* * Copyright © 2010 Codethink Limited + * Copyright © 2020 William Manley * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -1281,6 +1282,7 @@ random_instance_filler (GVariantSerialised *serialised, serialised->size = instance->size; serialised->depth = 0; + serialised->ordered_offsets_up_to = 0; g_assert_true (serialised->type_info == instance->type_info); g_assert_cmpuint (serialised->size, ==, instance->size); @@ -5143,6 +5145,47 @@ test_normal_checking_array_offsets (void) g_variant_unref (variant); } +/* This is a regression test that we can't have non-normal values that take up + * significantly more space than the normal equivalent, by specifying the + * offset table entries so that array elements overlap. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2121#note_832242 */ +static void +test_normal_checking_array_offsets2 (void) +{ + const guint8 data[] = { + 'h', 'i', '\0', + 0x03, 0x00, 0x03, + 0x06, 0x00, 0x06, + 0x09, 0x00, 0x09, + 0x0c, 0x00, 0x0c, + 0x0f, 0x00, 0x0f, + 0x12, 0x00, 0x12, + 0x15, 0x00, 0x15, + }; + gsize size = sizeof (data); + const GVariantType *aaaaaaas = G_VARIANT_TYPE ("aaaaaaas"); + GVariant *variant = NULL; + GVariant *normal_variant = NULL; + GVariant *expected = NULL; + + variant = g_variant_new_from_data (aaaaaaas, data, size, FALSE, NULL, NULL); + g_assert_nonnull (variant); + + normal_variant = g_variant_get_normal_form (variant); + g_assert_nonnull (normal_variant); + g_assert_cmpuint (g_variant_get_size (normal_variant), <=, size * 2); + + expected = g_variant_new_parsed ( + "[[[[[[['hi', '', ''], [], []], [], []], [], []], [], []], [], []], [], []]"); + g_assert_cmpvariant (expected, variant); + g_assert_cmpvariant (expected, normal_variant); + + g_variant_unref (expected); + g_variant_unref (normal_variant); + g_variant_unref (variant); +} + /* Test that a tuple with invalidly large values in its offset table is * normalised successfully without looping infinitely. */ static void @@ -5311,6 +5354,8 @@ main (int argc, char **argv) test_normal_checking_tuples); g_test_add_func ("/gvariant/normal-checking/array-offsets", test_normal_checking_array_offsets); + g_test_add_func ("/gvariant/normal-checking/array-offsets2", + test_normal_checking_array_offsets2); g_test_add_func ("/gvariant/normal-checking/tuple-offsets", test_normal_checking_tuple_offsets); g_test_add_func ("/gvariant/normal-checking/empty-object-path", -- GitLab From 345cae9c1aa7bf6752039225ef4c8d8d69fa8d76 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 7 Jan 2022 15:03:52 +0000 Subject: [PATCH 05/18] gvariant-serialiser: Factor out code to get bounds of a tuple member This introduces no functional changes. Signed-off-by: Philip Withnall Helps: #2121 --- glib/gvariant-serialiser.c | 73 ++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/glib/gvariant-serialiser.c b/glib/gvariant-serialiser.c index c33a903bff..5b67b3820f 100644 --- a/glib/gvariant-serialiser.c +++ b/glib/gvariant-serialiser.c @@ -944,6 +944,51 @@ gvs_variable_sized_array_is_normal (GVariantSerialised value) * for the tuple. See the notes in gvarianttypeinfo.h. */ +static void +gvs_tuple_get_member_bounds (GVariantSerialised value, + gsize index_, + gsize offset_size, + gsize *out_member_start, + gsize *out_member_end) +{ + const GVariantMemberInfo *member_info; + gsize member_start, member_end; + + member_info = g_variant_type_info_member_info (value.type_info, index_); + + if (member_info->i + 1) + member_start = gvs_read_unaligned_le (value.data + value.size - + offset_size * (member_info->i + 1), + offset_size); + else + member_start = 0; + + member_start += member_info->a; + member_start &= member_info->b; + member_start |= member_info->c; + + if (member_info->ending_type == G_VARIANT_MEMBER_ENDING_LAST) + member_end = value.size - offset_size * (member_info->i + 1); + + else if (member_info->ending_type == G_VARIANT_MEMBER_ENDING_FIXED) + { + gsize fixed_size; + + g_variant_type_info_query (member_info->type_info, NULL, &fixed_size); + member_end = member_start + fixed_size; + } + + else /* G_VARIANT_MEMBER_ENDING_OFFSET */ + member_end = gvs_read_unaligned_le (value.data + value.size - + offset_size * (member_info->i + 2), + offset_size); + + if (out_member_start != NULL) + *out_member_start = member_start; + if (out_member_end != NULL) + *out_member_end = member_end; +} + static gsize gvs_tuple_n_children (GVariantSerialised value) { @@ -999,33 +1044,7 @@ gvs_tuple_get_child (GVariantSerialised value, } } - if (member_info->i + 1) - start = gvs_read_unaligned_le (value.data + value.size - - offset_size * (member_info->i + 1), - offset_size); - else - start = 0; - - start += member_info->a; - start &= member_info->b; - start |= member_info->c; - - if (member_info->ending_type == G_VARIANT_MEMBER_ENDING_LAST) - end = value.size - offset_size * (member_info->i + 1); - - else if (member_info->ending_type == G_VARIANT_MEMBER_ENDING_FIXED) - { - gsize fixed_size; - - g_variant_type_info_query (child.type_info, NULL, &fixed_size); - end = start + fixed_size; - child.size = fixed_size; - } - - else /* G_VARIANT_MEMBER_ENDING_OFFSET */ - end = gvs_read_unaligned_le (value.data + value.size - - offset_size * (member_info->i + 2), - offset_size); + gvs_tuple_get_member_bounds (value, index_, offset_size, &start, &end); /* The child should not extend into the offset table. */ if (index_ != g_variant_type_info_n_members (value.type_info) - 1) -- GitLab From 73d0aa81c2575a5c9ae77dcb94da919579014fc0 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 7 Jan 2022 16:37:29 +0000 Subject: [PATCH 06/18] gvariant-serialiser: Rework child size calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reduces a few duplicate calls to `g_variant_type_info_query()` and explains why they’re needed. Signed-off-by: Philip Withnall Helps: #2121 --- glib/gvariant-serialiser.c | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/glib/gvariant-serialiser.c b/glib/gvariant-serialiser.c index 5b67b3820f..bc131b7514 100644 --- a/glib/gvariant-serialiser.c +++ b/glib/gvariant-serialiser.c @@ -1009,14 +1009,18 @@ gvs_tuple_get_child (GVariantSerialised value, child.depth = value.depth + 1; offset_size = gvs_get_offset_size (value.size); + /* Ensure the size is set for fixed-sized children, or + * g_variant_serialised_check() will fail, even if we return + * (child.data == NULL) to indicate an error. */ + if (member_info->ending_type == G_VARIANT_MEMBER_ENDING_FIXED) + g_variant_type_info_query (child.type_info, NULL, &child.size); + /* tuples are the only (potentially) fixed-sized containers, so the * only ones that have to deal with the possibility of having %NULL * data with a non-zero %size if errors occurred elsewhere. */ if G_UNLIKELY (value.data == NULL && value.size != 0) { - g_variant_type_info_query (child.type_info, NULL, &child.size); - /* this can only happen in fixed-sized tuples, * so the child must also be fixed sized. */ @@ -1034,29 +1038,12 @@ gvs_tuple_get_child (GVariantSerialised value, else { if (offset_size * (member_info->i + 1) > value.size) - { - /* if the child is fixed size, return its size. - * if child is not fixed-sized, return size = 0. - */ - g_variant_type_info_query (child.type_info, NULL, &child.size); - - return child; - } + return child; } - gvs_tuple_get_member_bounds (value, index_, offset_size, &start, &end); - /* The child should not extend into the offset table. */ - if (index_ != g_variant_type_info_n_members (value.type_info) - 1) - { - GVariantSerialised last_child; - last_child = gvs_tuple_get_child (value, - g_variant_type_info_n_members (value.type_info) - 1); - last_end = last_child.data + last_child.size - value.data; - g_variant_type_info_unref (last_child.type_info); - } - else - last_end = end; + gvs_tuple_get_member_bounds (value, index_, offset_size, &start, &end); + gvs_tuple_get_member_bounds (value, g_variant_type_info_n_members (value.type_info) - 1, offset_size, NULL, &last_end); if (start < end && end <= value.size && end <= last_end) { -- GitLab From 7cf6f5b69146d20948d42f0c476688fe17fef787 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 7 Jan 2022 16:42:14 +0000 Subject: [PATCH 07/18] =?UTF-8?q?gvariant:=20Don=E2=80=99t=20allow=20child?= =?UTF-8?q?=20elements=20of=20a=20tuple=20to=20overlap=20each=20other?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is similar to the earlier commit which prevents child elements of a variable-sized array from overlapping each other, but this time for tuples. It is based heavily on ideas by William Manley. Tuples are slightly different from variable-sized arrays in that they contain a mixture of fixed and variable sized elements. All but one of the variable sized elements have an entry in the frame offsets table. This means that if we were to just check the ordering of the frame offsets table, the variable sized elements could still overlap interleaving fixed sized elements, which would be bad. Therefore we have to check the elements rather than the frame offsets. The logic of checking the elements up to the index currently being requested, and caching the result in `ordered_offsets_up_to`, means that the algorithmic cost implications are the same for this commit as for variable-sized arrays: an O(N) cost for these checks is amortised out over N accesses to O(1) per access. Signed-off-by: Philip Withnall Fixes: #2121 --- glib/gvariant-core.c | 6 +- glib/gvariant-serialiser.c | 40 ++++++++ glib/gvariant-serialiser.h | 7 +- glib/gvariant.c | 1 + glib/tests/gvariant.c | 181 +++++++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 3 deletions(-) diff --git a/glib/gvariant-core.c b/glib/gvariant-core.c index f25d472794..4419371136 100644 --- a/glib/gvariant-core.c +++ b/glib/gvariant-core.c @@ -1,6 +1,7 @@ /* * Copyright © 2007, 2008 Ryan Lortie * Copyright © 2010 Codethink Limited + * Copyright © 2022 Endless OS Foundation, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -181,7 +182,7 @@ struct _GVariant * offsets themselves. * * This field is only relevant for arrays of non - * fixed width types. + * fixed width types and for tuples. * * .tree: Only valid when the instance is in tree form. * @@ -1141,6 +1142,9 @@ g_variant_get_child_value (GVariant *value, */ s_child = g_variant_serialised_get_child (serialised, index_); + /* Update the cached ordered_offsets_up_to, since @serialised will be thrown away when this function exits */ + value->contents.serialised.ordered_offsets_up_to = MAX (value->contents.serialised.ordered_offsets_up_to, serialised.ordered_offsets_up_to); + /* Check whether this would cause nesting too deep. If so, return a fake * child. The only situation we expect this to happen in is with a variant, * as all other deeply-nested types have a static type, and hence should diff --git a/glib/gvariant-serialiser.c b/glib/gvariant-serialiser.c index bc131b7514..263b74048c 100644 --- a/glib/gvariant-serialiser.c +++ b/glib/gvariant-serialiser.c @@ -944,6 +944,10 @@ gvs_variable_sized_array_is_normal (GVariantSerialised value) * for the tuple. See the notes in gvarianttypeinfo.h. */ +/* Note: This doesn’t guarantee that @out_member_end >= @out_member_start; that + * condition may not hold true for invalid serialised variants. The caller is + * responsible for checking the returned values and handling invalid ones + * appropriately. */ static void gvs_tuple_get_member_bounds (GVariantSerialised value, gsize index_, @@ -1030,6 +1034,42 @@ gvs_tuple_get_child (GVariantSerialised value, return child; } + /* If the requested @index_ is beyond the set of indices whose framing offsets + * have been checked, check the remaining offsets to see whether they’re + * normal (in order, no overlapping tuple elements). + * + * Unlike the checks in gvs_variable_sized_array_get_child(), we have to check + * all the tuple *elements* here, not just all the framing offsets, since + * tuples contain a mix of elements which use framing offsets and ones which + * don’t. None of them are allowed to overlap. */ + if (index_ > value.ordered_offsets_up_to) + { + gsize i, prev_i_end = 0; + + if (value.ordered_offsets_up_to > 0) + gvs_tuple_get_member_bounds (value, value.ordered_offsets_up_to - 1, offset_size, NULL, &prev_i_end); + + for (i = value.ordered_offsets_up_to; i <= index_; i++) + { + gsize i_start, i_end; + + gvs_tuple_get_member_bounds (value, i, offset_size, &i_start, &i_end); + + if (i_start > i_end || i_start < prev_i_end || i_end > value.size) + break; + + prev_i_end = i_end; + } + + value.ordered_offsets_up_to = i - 1; + } + + if (index_ > value.ordered_offsets_up_to) + { + /* Offsets are invalid somewhere, so return an empty child. */ + return child; + } + if (member_info->ending_type == G_VARIANT_MEMBER_ENDING_OFFSET) { if (offset_size * (member_info->i + 2) > value.size) diff --git a/glib/gvariant-serialiser.h b/glib/gvariant-serialiser.h index a1bccb834b..661bd8fd1b 100644 --- a/glib/gvariant-serialiser.h +++ b/glib/gvariant-serialiser.h @@ -37,8 +37,11 @@ typedef struct * This guarantees that the bytes of element n don't overlap with any previous * element. * - * This is both read and set by g_variant_serialised_get_child for arrays of - * non-fixed-width types */ + * This is both read and set by g_variant_serialised_get_child() for arrays of + * non-fixed-width types, and for tuples. + * + * Even when dealing with tuples, @ordered_offsets_up_to is an element index, + * rather than an index into the frame offsets. */ gsize ordered_offsets_up_to; } GVariantSerialised; diff --git a/glib/gvariant.c b/glib/gvariant.c index e4c85ca636..4f0d6b83c6 100644 --- a/glib/gvariant.c +++ b/glib/gvariant.c @@ -6011,6 +6011,7 @@ g_variant_byteswap (GVariant *value) serialised.size = g_variant_get_size (trusted); serialised.data = g_malloc (serialised.size); serialised.depth = g_variant_get_depth (trusted); + serialised.ordered_offsets_up_to = G_MAXSIZE; /* operating on the normal form */ g_variant_store (trusted, serialised.data); g_variant_unref (trusted); diff --git a/glib/tests/gvariant.c b/glib/tests/gvariant.c index 45e302c926..4ae52e2bb9 100644 --- a/glib/tests/gvariant.c +++ b/glib/tests/gvariant.c @@ -1,6 +1,7 @@ /* * Copyright © 2010 Codethink Limited * Copyright © 2020 William Manley + * Copyright © 2022 Endless OS Foundation, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -1449,6 +1450,7 @@ test_maybe (void) serialised.data = flavoured_malloc (needed_size, flavour); serialised.size = needed_size; serialised.depth = 0; + serialised.ordered_offsets_up_to = 0; g_variant_serialiser_serialise (serialised, random_instance_filler, @@ -1572,6 +1574,7 @@ test_array (void) serialised.data = flavoured_malloc (needed_size, flavour); serialised.size = needed_size; serialised.depth = 0; + serialised.ordered_offsets_up_to = 0; g_variant_serialiser_serialise (serialised, random_instance_filler, (gpointer *) instances, n_children); @@ -1736,6 +1739,7 @@ test_tuple (void) serialised.data = flavoured_malloc (needed_size, flavour); serialised.size = needed_size; serialised.depth = 0; + serialised.ordered_offsets_up_to = 0; g_variant_serialiser_serialise (serialised, random_instance_filler, (gpointer *) instances, n_children); @@ -1832,6 +1836,7 @@ test_variant (void) serialised.data = flavoured_malloc (needed_size, flavour); serialised.size = needed_size; serialised.depth = 0; + serialised.ordered_offsets_up_to = 0; g_variant_serialiser_serialise (serialised, random_instance_filler, (gpointer *) &instance, 1); @@ -5210,6 +5215,176 @@ test_normal_checking_tuple_offsets (void) g_variant_unref (variant); } +/* This is a regression test that we can't have non-normal values that take up + * significantly more space than the normal equivalent, by specifying the + * offset table entries so that tuple elements overlap. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2121#note_838503 and + * https://gitlab.gnome.org/GNOME/glib/-/issues/2121#note_838513 */ +static void +test_normal_checking_tuple_offsets2 (void) +{ + const GVariantType *data_type = G_VARIANT_TYPE ("(yyaiyyaiyy)"); + const guint8 data[] = { + 0x12, 0x34, 0x56, 0x78, 0x01, + /* + ^───────────────────┘ + + ^^^^^^^^^^ 1st yy + ^^^^^^^^^^ 2nd yy + ^^^^^^^^^^ 3rd yy + ^^^^ Framing offsets + */ + + /* If this variant was encoded normally, it would be something like this: + * 0x12, 0x34, pad, pad, [array bytes], 0x56, 0x78, pad, pad, [array bytes], 0x9A, 0xBC, 0xXX + * ^─────────────────────────────────────────────────────┘ + * + * ^^^^^^^^^^ 1st yy + * ^^^^^^^^^^ 2nd yy + * ^^^^^^^^^^ 3rd yy + * ^^^^ Framing offsets + */ + }; + gsize size = sizeof (data); + GVariant *variant = NULL; + GVariant *normal_variant = NULL; + GVariant *expected = NULL; + + variant = g_variant_new_from_data (data_type, data, size, FALSE, NULL, NULL); + g_assert_nonnull (variant); + + normal_variant = g_variant_get_normal_form (variant); + g_assert_nonnull (normal_variant); + g_assert_cmpuint (g_variant_get_size (normal_variant), <=, size * 3); + + expected = g_variant_new_parsed ( + "@(yyaiyyaiyy) (0x12, 0x34, [], 0x00, 0x00, [], 0x00, 0x00)"); + g_assert_cmpvariant (expected, variant); + g_assert_cmpvariant (expected, normal_variant); + + g_variant_unref (expected); + g_variant_unref (normal_variant); + g_variant_unref (variant); +} + +/* This is a regression test that overlapping entries in the offset table are + * decoded consistently, even though they’re non-normal. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2121#note_910935 */ +static void +test_normal_checking_tuple_offsets3 (void) +{ + /* The expected decoding of this non-normal byte stream is complex. See + * section 2.7.3 (Handling Non-Normal Serialised Data) of the GVariant + * specification. + * + * The rule “Child Values Overlapping Framing Offsets” from the specification + * says that the first `ay` must be decoded as `[0x01]` even though it + * overlaps the first byte of the offset table. However, since commit + * 7eedcd76f7d5b8c98fa60013e1fe6e960bf19df3, GLib explicitly doesn’t allow + * this as it’s exploitable. So the first `ay` must be given a default value. + * + * The second and third `ay`s must be given default values because of rule + * “End Boundary Precedes Start Boundary”. + * + * The `i` must be given a default value because of rule “Start or End + * Boundary of a Child Falls Outside the Container”. + */ + const GVariantType *data_type = G_VARIANT_TYPE ("(ayayiay)"); + const guint8 data[] = { + 0x01, 0x00, 0x02, + /* + ^──┘ + + ^^^^^^^^^^ 1st ay, bytes 0-2 (but given a default value anyway, see above) + 2nd ay, bytes 2-0 + i, bytes 0-4 + 3rd ay, bytes 4-1 + ^^^^^^^^^^ Framing offsets + */ + }; + gsize size = sizeof (data); + GVariant *variant = NULL; + GVariant *normal_variant = NULL; + GVariant *expected = NULL; + + variant = g_variant_new_from_data (data_type, data, size, FALSE, NULL, NULL); + g_assert_nonnull (variant); + + g_assert_false (g_variant_is_normal_form (variant)); + + normal_variant = g_variant_get_normal_form (variant); + g_assert_nonnull (normal_variant); + g_assert_cmpuint (g_variant_get_size (normal_variant), <=, size * 3); + + expected = g_variant_new_parsed ("@(ayayiay) ([], [], 0, [])"); + g_assert_cmpvariant (expected, variant); + g_assert_cmpvariant (expected, normal_variant); + + g_variant_unref (expected); + g_variant_unref (normal_variant); + g_variant_unref (variant); +} + +/* This is a regression test that overlapping entries in the offset table are + * decoded consistently, even though they’re non-normal. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2121#note_910935 */ +static void +test_normal_checking_tuple_offsets4 (void) +{ + /* The expected decoding of this non-normal byte stream is complex. See + * section 2.7.3 (Handling Non-Normal Serialised Data) of the GVariant + * specification. + * + * The rule “Child Values Overlapping Framing Offsets” from the specification + * says that the first `ay` must be decoded as `[0x01]` even though it + * overlaps the first byte of the offset table. However, since commit + * 7eedcd76f7d5b8c98fa60013e1fe6e960bf19df3, GLib explicitly doesn’t allow + * this as it’s exploitable. So the first `ay` must be given a default value. + * + * The second `ay` must be given a default value because of rule “End Boundary + * Precedes Start Boundary”. + * + * The third `ay` must be given a default value because its framing offsets + * overlap that of the first `ay`. + */ + const GVariantType *data_type = G_VARIANT_TYPE ("(ayayay)"); + const guint8 data[] = { + 0x01, 0x00, 0x02, + /* + ^──┘ + + ^^^^^^^^^^ 1st ay, bytes 0-2 (but given a default value anyway, see above) + 2nd ay, bytes 2-0 + 3rd ay, bytes 0-1 + ^^^^^^^^^^ Framing offsets + */ + }; + gsize size = sizeof (data); + GVariant *variant = NULL; + GVariant *normal_variant = NULL; + GVariant *expected = NULL; + + variant = g_variant_new_from_data (data_type, data, size, FALSE, NULL, NULL); + g_assert_nonnull (variant); + + g_assert_false (g_variant_is_normal_form (variant)); + + normal_variant = g_variant_get_normal_form (variant); + g_assert_nonnull (normal_variant); + g_assert_cmpuint (g_variant_get_size (normal_variant), <=, size * 3); + + expected = g_variant_new_parsed ("@(ayayay) ([], [], [])"); + g_assert_cmpvariant (expected, variant); + g_assert_cmpvariant (expected, normal_variant); + + g_variant_unref (expected); + g_variant_unref (normal_variant); + g_variant_unref (variant); +} + /* Test that an empty object path is normalised successfully to the base object * path, ‘/’. */ static void @@ -5358,6 +5533,12 @@ main (int argc, char **argv) test_normal_checking_array_offsets2); g_test_add_func ("/gvariant/normal-checking/tuple-offsets", test_normal_checking_tuple_offsets); + g_test_add_func ("/gvariant/normal-checking/tuple-offsets2", + test_normal_checking_tuple_offsets2); + g_test_add_func ("/gvariant/normal-checking/tuple-offsets3", + test_normal_checking_tuple_offsets3); + g_test_add_func ("/gvariant/normal-checking/tuple-offsets4", + test_normal_checking_tuple_offsets4); g_test_add_func ("/gvariant/normal-checking/empty-object-path", test_normal_checking_empty_object_path); -- GitLab From d1a293c4e29880b8d17bb826c9a426a440ca4a91 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 25 Oct 2022 15:14:14 +0100 Subject: [PATCH 08/18] gvariant: Track checked and ordered offsets independently The past few commits introduced the concept of known-good offsets in the offset table (which is used for variable-width arrays and tuples). Good offsets are ones which are non-overlapping with all the previous offsets in the table. If a bad offset is encountered when indexing into the array or tuple, the cached known-good offset index will not be increased. In this way, all child variants at and beyond the first bad offset can be returned as default values rather than dereferencing potentially invalid data. In this case, there was no information about the fact that the indexes between the highest known-good index and the requested one had been checked already. That could lead to a pathological case where an offset table with an invalid first offset is repeatedly checked in full when trying to access higher-indexed children. Avoid that by storing the index of the highest checked offset in the table, as well as the index of the highest good/ordered offset. Signed-off-by: Philip Withnall Helps: #2121 --- glib/gvariant-core.c | 28 ++++++++++++++++++++++++ glib/gvariant-serialiser.c | 44 +++++++++++++++++++++++++++----------- glib/gvariant-serialiser.h | 9 ++++++++ glib/gvariant.c | 1 + glib/tests/gvariant.c | 5 +++++ 5 files changed, 75 insertions(+), 12 deletions(-) diff --git a/glib/gvariant-core.c b/glib/gvariant-core.c index 4419371136..f660e2d578 100644 --- a/glib/gvariant-core.c +++ b/glib/gvariant-core.c @@ -69,6 +69,7 @@ struct _GVariant GBytes *bytes; gconstpointer data; gsize ordered_offsets_up_to; + gsize checked_offsets_up_to; } serialised; struct @@ -184,6 +185,24 @@ struct _GVariant * This field is only relevant for arrays of non * fixed width types and for tuples. * + * .checked_offsets_up_to: Similarly to .ordered_offsets_up_to, this stores + * the index of the highest element, n, whose frame + * offsets (and all the preceding frame offsets) + * have been checked for validity. + * + * It is always the case that + * .checked_offsets_up_to ≥ .ordered_offsets_up_to. + * + * If .checked_offsets_up_to == .ordered_offsets_up_to, + * then a bad offset has not been found so far. + * + * If .checked_offsets_up_to > .ordered_offsets_up_to, + * then a bad offset has been found at + * (.ordered_offsets_up_to + 1). + * + * This field is only relevant for arrays of non + * fixed width types and for tuples. + * * .tree: Only valid when the instance is in tree form. * * Note that accesses from other threads could result in @@ -388,6 +407,7 @@ g_variant_to_serialised (GVariant *value) value->size, value->depth, value->contents.serialised.ordered_offsets_up_to, + value->contents.serialised.checked_offsets_up_to, }; return serialised; } @@ -420,6 +440,7 @@ g_variant_serialise (GVariant *value, serialised.data = data; serialised.depth = value->depth; serialised.ordered_offsets_up_to = 0; + serialised.checked_offsets_up_to = 0; children = (gpointer *) value->contents.tree.children; n_children = value->contents.tree.n_children; @@ -466,10 +487,12 @@ g_variant_fill_gvs (GVariantSerialised *serialised, if (value->state & STATE_SERIALISED) { serialised->ordered_offsets_up_to = value->contents.serialised.ordered_offsets_up_to; + serialised->checked_offsets_up_to = value->contents.serialised.checked_offsets_up_to; } else { serialised->ordered_offsets_up_to = 0; + serialised->checked_offsets_up_to = 0; } if (serialised->data) @@ -515,6 +538,7 @@ g_variant_ensure_serialised (GVariant *value) value->contents.serialised.data = g_bytes_get_data (bytes, NULL); value->contents.serialised.bytes = bytes; value->contents.serialised.ordered_offsets_up_to = G_MAXSIZE; + value->contents.serialised.checked_offsets_up_to = G_MAXSIZE; value->state |= STATE_SERIALISED; } } @@ -596,6 +620,7 @@ g_variant_new_from_bytes (const GVariantType *type, serialised.data = (guchar *) g_bytes_get_data (bytes, &serialised.size); serialised.depth = 0; serialised.ordered_offsets_up_to = trusted ? G_MAXSIZE : 0; + serialised.checked_offsets_up_to = trusted ? G_MAXSIZE : 0; if (!g_variant_serialised_check (serialised)) { @@ -647,6 +672,7 @@ g_variant_new_from_bytes (const GVariantType *type, } value->contents.serialised.ordered_offsets_up_to = trusted ? G_MAXSIZE : 0; + value->contents.serialised.checked_offsets_up_to = trusted ? G_MAXSIZE : 0; g_clear_pointer (&owned_bytes, g_bytes_unref); @@ -1144,6 +1170,7 @@ g_variant_get_child_value (GVariant *value, /* Update the cached ordered_offsets_up_to, since @serialised will be thrown away when this function exits */ value->contents.serialised.ordered_offsets_up_to = MAX (value->contents.serialised.ordered_offsets_up_to, serialised.ordered_offsets_up_to); + value->contents.serialised.checked_offsets_up_to = MAX (value->contents.serialised.checked_offsets_up_to, serialised.checked_offsets_up_to); /* Check whether this would cause nesting too deep. If so, return a fake * child. The only situation we expect this to happen in is with a variant, @@ -1171,6 +1198,7 @@ g_variant_get_child_value (GVariant *value, g_bytes_ref (value->contents.serialised.bytes); child->contents.serialised.data = s_child.data; child->contents.serialised.ordered_offsets_up_to = s_child.ordered_offsets_up_to; + child->contents.serialised.checked_offsets_up_to = s_child.checked_offsets_up_to; return child; } diff --git a/glib/gvariant-serialiser.c b/glib/gvariant-serialiser.c index 263b74048c..69baeeb395 100644 --- a/glib/gvariant-serialiser.c +++ b/glib/gvariant-serialiser.c @@ -122,6 +122,8 @@ * * @depth has no restrictions; the depth of a top-level serialized #GVariant is * zero, and it increases for each level of nested child. + * + * @checked_offsets_up_to is always ≥ @ordered_offsets_up_to */ /* < private > @@ -149,6 +151,9 @@ g_variant_serialised_check (GVariantSerialised serialised) !(serialised.size == 0 || serialised.data != NULL)) return FALSE; + if (serialised.ordered_offsets_up_to > serialised.checked_offsets_up_to) + return FALSE; + /* Depending on the native alignment requirements of the machine, the * compiler will insert either 3 or 7 padding bytes after the char. * This will result in the sizeof() the struct being 12 or 16. @@ -268,6 +273,7 @@ gvs_fixed_sized_maybe_get_child (GVariantSerialised value, g_variant_type_info_ref (value.type_info); value.depth++; value.ordered_offsets_up_to = 0; + value.checked_offsets_up_to = 0; return value; } @@ -299,7 +305,7 @@ gvs_fixed_sized_maybe_serialise (GVariantSerialised value, { if (n_children) { - GVariantSerialised child = { NULL, value.data, value.size, value.depth + 1, 0 }; + GVariantSerialised child = { NULL, value.data, value.size, value.depth + 1, 0, 0 }; gvs_filler (&child, children[0]); } @@ -322,6 +328,7 @@ gvs_fixed_sized_maybe_is_normal (GVariantSerialised value) value.type_info = g_variant_type_info_element (value.type_info); value.depth++; value.ordered_offsets_up_to = 0; + value.checked_offsets_up_to = 0; return g_variant_serialised_is_normal (value); } @@ -364,6 +371,7 @@ gvs_variable_sized_maybe_get_child (GVariantSerialised value, value.depth++; value.ordered_offsets_up_to = 0; + value.checked_offsets_up_to = 0; return value; } @@ -394,7 +402,7 @@ gvs_variable_sized_maybe_serialise (GVariantSerialised value, { if (n_children) { - GVariantSerialised child = { NULL, value.data, value.size - 1, value.depth + 1, 0 }; + GVariantSerialised child = { NULL, value.data, value.size - 1, value.depth + 1, 0, 0 }; /* write the data for the child. */ gvs_filler (&child, children[0]); @@ -415,6 +423,7 @@ gvs_variable_sized_maybe_is_normal (GVariantSerialised value) value.size--; value.depth++; value.ordered_offsets_up_to = 0; + value.checked_offsets_up_to = 0; return g_variant_serialised_is_normal (value); } @@ -741,39 +750,46 @@ gvs_variable_sized_array_get_child (GVariantSerialised value, /* If the requested @index_ is beyond the set of indices whose framing offsets * have been checked, check the remaining offsets to see whether they’re - * normal (in order, no overlapping array elements). */ - if (index_ > value.ordered_offsets_up_to) + * normal (in order, no overlapping array elements). + * + * Don’t bother checking if the highest known-good offset is lower than the + * highest checked offset, as that means there’s an invalid element at that + * index, so there’s no need to check further. */ + if (index_ > value.checked_offsets_up_to && + value.ordered_offsets_up_to == value.checked_offsets_up_to) { switch (offsets.offset_size) { case 1: { value.ordered_offsets_up_to = find_unordered_guint8 ( - offsets.array, value.ordered_offsets_up_to, index_ + 1); + offsets.array, value.checked_offsets_up_to, index_ + 1); break; } case 2: { value.ordered_offsets_up_to = find_unordered_guint16 ( - offsets.array, value.ordered_offsets_up_to, index_ + 1); + offsets.array, value.checked_offsets_up_to, index_ + 1); break; } case 4: { value.ordered_offsets_up_to = find_unordered_guint32 ( - offsets.array, value.ordered_offsets_up_to, index_ + 1); + offsets.array, value.checked_offsets_up_to, index_ + 1); break; } case 8: { value.ordered_offsets_up_to = find_unordered_guint64 ( - offsets.array, value.ordered_offsets_up_to, index_ + 1); + offsets.array, value.checked_offsets_up_to, index_ + 1); break; } default: /* gvs_get_offset_size() only returns maximum 8 */ g_assert_not_reached (); } + + value.checked_offsets_up_to = index_; } if (index_ > value.ordered_offsets_up_to) @@ -918,6 +934,7 @@ gvs_variable_sized_array_is_normal (GVariantSerialised value) /* All offsets have now been checked. */ value.ordered_offsets_up_to = G_MAXSIZE; + value.checked_offsets_up_to = G_MAXSIZE; return TRUE; } @@ -1042,14 +1059,15 @@ gvs_tuple_get_child (GVariantSerialised value, * all the tuple *elements* here, not just all the framing offsets, since * tuples contain a mix of elements which use framing offsets and ones which * don’t. None of them are allowed to overlap. */ - if (index_ > value.ordered_offsets_up_to) + if (index_ > value.checked_offsets_up_to && + value.ordered_offsets_up_to == value.checked_offsets_up_to) { gsize i, prev_i_end = 0; - if (value.ordered_offsets_up_to > 0) - gvs_tuple_get_member_bounds (value, value.ordered_offsets_up_to - 1, offset_size, NULL, &prev_i_end); + if (value.checked_offsets_up_to > 0) + gvs_tuple_get_member_bounds (value, value.checked_offsets_up_to - 1, offset_size, NULL, &prev_i_end); - for (i = value.ordered_offsets_up_to; i <= index_; i++) + for (i = value.checked_offsets_up_to; i <= index_; i++) { gsize i_start, i_end; @@ -1062,6 +1080,7 @@ gvs_tuple_get_child (GVariantSerialised value, } value.ordered_offsets_up_to = i - 1; + value.checked_offsets_up_to = index_; } if (index_ > value.ordered_offsets_up_to) @@ -1263,6 +1282,7 @@ gvs_tuple_is_normal (GVariantSerialised value) /* All element bounds have been checked above. */ value.ordered_offsets_up_to = G_MAXSIZE; + value.checked_offsets_up_to = G_MAXSIZE; { gsize fixed_size; diff --git a/glib/gvariant-serialiser.h b/glib/gvariant-serialiser.h index 661bd8fd1b..eb74fe7804 100644 --- a/glib/gvariant-serialiser.h +++ b/glib/gvariant-serialiser.h @@ -43,6 +43,15 @@ typedef struct * Even when dealing with tuples, @ordered_offsets_up_to is an element index, * rather than an index into the frame offsets. */ gsize ordered_offsets_up_to; + + /* Similar to @ordered_offsets_up_to. This gives the index of the child element + * whose frame offset is the highest in the offset table which has been + * checked so far. + * + * This is always ≥ @ordered_offsets_up_to. It is always an element index. + * + * See documentation in gvariant-core.c for `struct GVariant` for details. */ + gsize checked_offsets_up_to; } GVariantSerialised; /* deserialization */ diff --git a/glib/gvariant.c b/glib/gvariant.c index 4f0d6b83c6..fd066f1732 100644 --- a/glib/gvariant.c +++ b/glib/gvariant.c @@ -6012,6 +6012,7 @@ g_variant_byteswap (GVariant *value) serialised.data = g_malloc (serialised.size); serialised.depth = g_variant_get_depth (trusted); serialised.ordered_offsets_up_to = G_MAXSIZE; /* operating on the normal form */ + serialised.checked_offsets_up_to = G_MAXSIZE; g_variant_store (trusted, serialised.data); g_variant_unref (trusted); diff --git a/glib/tests/gvariant.c b/glib/tests/gvariant.c index 4ae52e2bb9..d82aedd509 100644 --- a/glib/tests/gvariant.c +++ b/glib/tests/gvariant.c @@ -1284,6 +1284,7 @@ random_instance_filler (GVariantSerialised *serialised, serialised->depth = 0; serialised->ordered_offsets_up_to = 0; + serialised->checked_offsets_up_to = 0; g_assert_true (serialised->type_info == instance->type_info); g_assert_cmpuint (serialised->size, ==, instance->size); @@ -1451,6 +1452,7 @@ test_maybe (void) serialised.size = needed_size; serialised.depth = 0; serialised.ordered_offsets_up_to = 0; + serialised.checked_offsets_up_to = 0; g_variant_serialiser_serialise (serialised, random_instance_filler, @@ -1575,6 +1577,7 @@ test_array (void) serialised.size = needed_size; serialised.depth = 0; serialised.ordered_offsets_up_to = 0; + serialised.checked_offsets_up_to = 0; g_variant_serialiser_serialise (serialised, random_instance_filler, (gpointer *) instances, n_children); @@ -1740,6 +1743,7 @@ test_tuple (void) serialised.size = needed_size; serialised.depth = 0; serialised.ordered_offsets_up_to = 0; + serialised.checked_offsets_up_to = 0; g_variant_serialiser_serialise (serialised, random_instance_filler, (gpointer *) instances, n_children); @@ -1837,6 +1841,7 @@ test_variant (void) serialised.size = needed_size; serialised.depth = 0; serialised.ordered_offsets_up_to = 0; + serialised.checked_offsets_up_to = 0; g_variant_serialiser_serialise (serialised, random_instance_filler, (gpointer *) &instance, 1); -- GitLab From 6fa41d5bf61a249a395a83866c10fa4a79bbb7db Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 12 Jun 2020 18:01:13 +0100 Subject: [PATCH 09/18] tests: Add another test for overlapping offsets in GVariant Signed-off-by: Philip Withnall Helps: #2121 --- glib/tests/gvariant.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/glib/tests/gvariant.c b/glib/tests/gvariant.c index d82aedd509..8ec94814e4 100644 --- a/glib/tests/gvariant.c +++ b/glib/tests/gvariant.c @@ -5131,6 +5131,38 @@ test_recursion_limits_array_in_variant (void) g_variant_unref (wrapper_variant); } +/* Test that a nested array with invalid values in its offset table (which point + * from the inner to the outer array) is normalised successfully without + * looping infinitely. */ +static void +test_normal_checking_array_offsets_overlapped (void) +{ + const guint8 data[] = { + 0x01, 0x00, + }; + gsize size = sizeof (data); + GVariant *variant = NULL; + GVariant *normal_variant = NULL; + GVariant *expected_variant = NULL; + + variant = g_variant_new_from_data (G_VARIANT_TYPE ("aay"), data, size, + FALSE, NULL, NULL); + g_assert_nonnull (variant); + + normal_variant = g_variant_get_normal_form (variant); + g_assert_nonnull (normal_variant); + + expected_variant = g_variant_new_parsed ("[@ay [], []]"); + g_assert_cmpvariant (normal_variant, expected_variant); + + g_assert_cmpmem (g_variant_get_data (normal_variant), g_variant_get_size (normal_variant), + g_variant_get_data (expected_variant), g_variant_get_size (expected_variant)); + + g_variant_unref (expected_variant); + g_variant_unref (normal_variant); + g_variant_unref (variant); +} + /* Test that an array with invalidly large values in its offset table is * normalised successfully without looping infinitely. */ static void @@ -5532,6 +5564,8 @@ main (int argc, char **argv) g_test_add_func ("/gvariant/normal-checking/tuples", test_normal_checking_tuples); + g_test_add_func ("/gvariant/normal-checking/array-offsets/overlapped", + test_normal_checking_array_offsets_overlapped); g_test_add_func ("/gvariant/normal-checking/array-offsets", test_normal_checking_array_offsets); g_test_add_func ("/gvariant/normal-checking/array-offsets2", -- GitLab From 4c0ddb26bc3f27eddf802b35c4b5229c0cdf6085 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 24 Oct 2022 16:43:23 +0100 Subject: [PATCH 10/18] tests: Disable some random instance tests of GVariants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building a `GVariant` using entirely random data may result in a non-normally-formed `GVariant`. It’s always possible to read these `GVariant`s, but the API might return default values for some or all of their components. In particular, this can easily happen when randomly generating the offset tables for non-fixed-width container types. If it does happen, bytewise comparison of the parsed `GVariant` with the original bytes will not always match. So skip those checks. Signed-off-by: Philip Withnall Helps: #2121 --- glib/tests/gvariant.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/glib/tests/gvariant.c b/glib/tests/gvariant.c index 8ec94814e4..18aeb80f12 100644 --- a/glib/tests/gvariant.c +++ b/glib/tests/gvariant.c @@ -1231,6 +1231,7 @@ random_instance_assert (RandomInstance *instance, GRand *rand; gsize i; + g_assert_true (size == 0 || buffer != NULL); g_assert_cmpint ((gsize) buffer & ALIGN_BITS & instance->alignment, ==, 0); g_assert_cmpint (size, ==, instance->size); @@ -1457,10 +1458,13 @@ test_maybe (void) g_variant_serialiser_serialise (serialised, random_instance_filler, (gpointer *) &instance, 1); + child = g_variant_serialised_get_child (serialised, 0); g_assert_true (child.type_info == instance->type_info); - random_instance_assert (instance, child.data, child.size); + if (child.data != NULL) /* could be NULL if element is non-normal */ + random_instance_assert (instance, child.data, child.size); g_variant_type_info_unref (child.type_info); + flavoured_free (serialised.data, flavour); } } @@ -1593,7 +1597,8 @@ test_array (void) child = g_variant_serialised_get_child (serialised, i); g_assert_true (child.type_info == instances[i]->type_info); - random_instance_assert (instances[i], child.data, child.size); + if (child.data != NULL) /* could be NULL if element is non-normal */ + random_instance_assert (instances[i], child.data, child.size); g_variant_type_info_unref (child.type_info); } @@ -1759,7 +1764,8 @@ test_tuple (void) child = g_variant_serialised_get_child (serialised, i); g_assert_true (child.type_info == instances[i]->type_info); - random_instance_assert (instances[i], child.data, child.size); + if (child.data != NULL) /* could be NULL if element is non-normal */ + random_instance_assert (instances[i], child.data, child.size); g_variant_type_info_unref (child.type_info); } -- GitLab From 35dee77ed887a9b4d24b7a0e45f237f813e52591 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 24 Oct 2022 18:14:57 +0100 Subject: [PATCH 11/18] gvariant: Clarify the docs for g_variant_get_normal_form() Document how non-normal parts of the `GVariant` are handled. Signed-off-by: Philip Withnall --- glib/gvariant.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/glib/gvariant.c b/glib/gvariant.c index fd066f1732..a98f10b57a 100644 --- a/glib/gvariant.c +++ b/glib/gvariant.c @@ -5936,7 +5936,9 @@ g_variant_deep_copy (GVariant *value) * marked as trusted and a new reference to it is returned. * * If @value is found not to be in normal form then a new trusted - * #GVariant is created with the same value as @value. + * #GVariant is created with the same value as @value. The non-normal parts of + * @value will be replaced with default values which are guaranteed to be in + * normal form. * * It makes sense to call this function if you've received #GVariant * data from untrusted sources and you want to ensure your serialized -- GitLab From e6490c84e84ba9f182fbd83b51ff4f9f5a0a1793 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 24 Oct 2022 18:43:55 +0100 Subject: [PATCH 12/18] gvariant: Port g_variant_deep_copy() to count its iterations directly This is equivalent to what `GVariantIter` does, but it means that `g_variant_deep_copy()` is making its own `g_variant_get_child_value()` calls. This will be useful in an upcoming commit, where those child values will be inspected a little more deeply. Signed-off-by: Philip Withnall Helps: #2121 --- glib/gvariant.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/glib/gvariant.c b/glib/gvariant.c index a98f10b57a..9334ec393e 100644 --- a/glib/gvariant.c +++ b/glib/gvariant.c @@ -5863,14 +5863,13 @@ g_variant_deep_copy (GVariant *value) case G_VARIANT_CLASS_VARIANT: { GVariantBuilder builder; - GVariantIter iter; - GVariant *child; + gsize i, n_children; g_variant_builder_init (&builder, g_variant_get_type (value)); - g_variant_iter_init (&iter, value); - while ((child = g_variant_iter_next_value (&iter))) + for (i = 0, n_children = g_variant_n_children (value); i < n_children; i++) { + GVariant *child = g_variant_get_child_value (value, i); g_variant_builder_add_value (&builder, g_variant_deep_copy (child)); g_variant_unref (child); } -- GitLab From 168f9b42e55053611ad473898f7afb06ce20b08a Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 25 Oct 2022 13:03:22 +0100 Subject: [PATCH 13/18] gvariant: Add internal g_variant_maybe_get_child_value() This will be used in a following commit. Signed-off-by: Philip Withnall Helps: #2540 --- glib/gvariant-core.c | 68 ++++++++++++++++++++++++++++++++++++++++++++ glib/gvariant-core.h | 3 ++ 2 files changed, 71 insertions(+) diff --git a/glib/gvariant-core.c b/glib/gvariant-core.c index f660e2d578..3fa97f91eb 100644 --- a/glib/gvariant-core.c +++ b/glib/gvariant-core.c @@ -1204,6 +1204,74 @@ g_variant_get_child_value (GVariant *value, } } +/** + * g_variant_maybe_get_child_value: + * @value: a container #GVariant + * @index_: the index of the child to fetch + * + * Reads a child item out of a container #GVariant instance, if it is in normal + * form. If it is not in normal form, return %NULL. + * + * This function behaves the same as g_variant_get_child_value(), except that it + * returns %NULL if the child is not in normal form. g_variant_get_child_value() + * would instead return a new default value of the correct type. + * + * This is intended to be used internally to avoid unnecessary #GVariant + * allocations. + * + * The returned value is never floating. You should free it with + * g_variant_unref() when you're done with it. + * + * This function is O(1). + * + * Returns: (transfer full): the child at the specified index + * + * Since: 2.74 + */ +GVariant * +g_variant_maybe_get_child_value (GVariant *value, + gsize index_) +{ + g_return_val_if_fail (index_ < g_variant_n_children (value), NULL); + g_return_val_if_fail (value->depth < G_MAXSIZE, NULL); + + if (~g_atomic_int_get (&value->state) & STATE_SERIALISED) + { + g_variant_lock (value); + + if (~value->state & STATE_SERIALISED) + { + GVariant *child; + + child = g_variant_ref (value->contents.tree.children[index_]); + g_variant_unlock (value); + + return child; + } + + g_variant_unlock (value); + } + + { + GVariantSerialised serialised = g_variant_to_serialised (value); + GVariantSerialised s_child; + + /* get the serializer to extract the serialized data for the child + * from the serialized data for the container + */ + s_child = g_variant_serialised_get_child (serialised, index_); + + if (!(value->state & STATE_TRUSTED) && s_child.data == NULL) + { + g_variant_type_info_unref (s_child.type_info); + return NULL; + } + + g_variant_type_info_unref (s_child.type_info); + return g_variant_get_child_value (value, index_); + } +} + /** * g_variant_store: * @value: the #GVariant to store diff --git a/glib/gvariant-core.h b/glib/gvariant-core.h index a1c34739a3..fb5f4bff45 100644 --- a/glib/gvariant-core.h +++ b/glib/gvariant-core.h @@ -38,4 +38,7 @@ GVariantTypeInfo * g_variant_get_type_info (GVarian gsize g_variant_get_depth (GVariant *value); +GVariant * g_variant_maybe_get_child_value (GVariant *value, + gsize index_); + #endif /* __G_VARIANT_CORE_H__ */ -- GitLab From c2dc74e2ec9b846d88ac4dd4368a9e2ab377cf94 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 25 Oct 2022 13:03:45 +0100 Subject: [PATCH 14/18] gvariant: Cut allocs of default values for children of non-normal arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This improves a slow case in `g_variant_get_normal_form()` where allocating many identical default values for the children of a variable-sized array which has a malformed offset table would take a lot of time. The fix is to make all child values after the first invalid one be references to the default value emitted for the first invalid one, rather than identical new `GVariant`s. In particular, this fixes a case where an attacker could create an array of length L of very large tuples of size T each, corrupt the offset table so they don’t have to specify the array content, and then induce `g_variant_get_normal_form()` into allocating L×T default values from an input which is significantly smaller than L×T in length. A pre-existing workaround for this issue is for code to call `g_variant_is_normal_form()` before calling `g_variant_get_normal_form()`, and to skip the latter call if the former returns false. This commit improves the behaviour in the case that `g_variant_get_normal_form()` is called anyway. This fix changes the time to run the `fuzz_variant_binary` test on the testcase from oss-fuzz#19777 from >60s (before being terminated) with 2.3GB of memory usage and 580k page faults; to 32s, 8.3MB of memory usage and 1500 page faults (as measured by `time -v`). Signed-off-by: Philip Withnall Fixes: #2540 oss-fuzz#19777 --- glib/gvariant.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/glib/gvariant.c b/glib/gvariant.c index 9334ec393e..356e199a0f 100644 --- a/glib/gvariant.c +++ b/glib/gvariant.c @@ -5857,7 +5857,6 @@ g_variant_deep_copy (GVariant *value) switch (g_variant_classify (value)) { case G_VARIANT_CLASS_MAYBE: - case G_VARIANT_CLASS_ARRAY: case G_VARIANT_CLASS_TUPLE: case G_VARIANT_CLASS_DICT_ENTRY: case G_VARIANT_CLASS_VARIANT: @@ -5877,6 +5876,71 @@ g_variant_deep_copy (GVariant *value) return g_variant_builder_end (&builder); } + case G_VARIANT_CLASS_ARRAY: + { + GVariantBuilder builder; + gsize i, n_children; + GVariant *first_invalid_child_deep_copy = NULL; + + /* Arrays are in theory treated the same as maybes, tuples, dict entries + * and variants, and could be another case in the above block of code. + * + * However, they have the property that when dealing with non-normal + * data (which is the only time g_variant_deep_copy() is currently + * called) in a variable-sized array, the code above can easily end up + * creating many default child values in order to return an array which + * is of the right length and type, but without containing non-normal + * data. This can happen if the offset table for the array is malformed. + * + * In this case, the code above would end up allocating the same default + * value for each one of the child indexes beyond the first malformed + * entry in the offset table. This can end up being a lot of identical + * allocations of default values, particularly if the non-normal array + * is crafted maliciously. + * + * Avoid that problem by returning a new reference to the same default + * value for every child after the first invalid one. This results in + * returning an equivalent array, in normal form and trusted — but with + * significantly fewer memory allocations. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2540 */ + + g_variant_builder_init (&builder, g_variant_get_type (value)); + + for (i = 0, n_children = g_variant_n_children (value); i < n_children; i++) + { + /* Try maybe_get_child_value() first; if it returns NULL, this child + * is non-normal. get_child_value() would have constructed and + * returned a default value in that case. */ + GVariant *child = g_variant_maybe_get_child_value (value, i); + + if (child != NULL) + { + /* Non-normal children may not always be contiguous, as they may + * be non-normal for reasons other than invalid offset table + * entries. As they are all the same type, they will all have + * the same default value though, so keep that around. */ + g_variant_builder_add_value (&builder, g_variant_deep_copy (child)); + } + else if (child == NULL && first_invalid_child_deep_copy != NULL) + { + g_variant_builder_add_value (&builder, first_invalid_child_deep_copy); + } + else if (child == NULL) + { + child = g_variant_get_child_value (value, i); + first_invalid_child_deep_copy = g_variant_ref_sink (g_variant_deep_copy (child)); + g_variant_builder_add_value (&builder, first_invalid_child_deep_copy); + } + + g_clear_pointer (&child, g_variant_unref); + } + + g_clear_pointer (&first_invalid_child_deep_copy, g_variant_unref); + + return g_variant_builder_end (&builder); + } + case G_VARIANT_CLASS_BOOLEAN: return g_variant_new_boolean (g_variant_get_boolean (value)); -- GitLab From f98c60e4eedf775fb896c89bc83c71c96224f6aa Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 25 Oct 2022 18:03:56 +0100 Subject: [PATCH 15/18] gvariant: Fix a leak of a GVariantTypeInfo on an error handling path Signed-off-by: Philip Withnall --- glib/gvariant-core.c | 1 + 1 file changed, 1 insertion(+) diff --git a/glib/gvariant-core.c b/glib/gvariant-core.c index 3fa97f91eb..f441c4757e 100644 --- a/glib/gvariant-core.c +++ b/glib/gvariant-core.c @@ -1183,6 +1183,7 @@ g_variant_get_child_value (GVariant *value, G_VARIANT_MAX_RECURSION_DEPTH - value->depth) { g_assert (g_variant_is_of_type (value, G_VARIANT_TYPE_VARIANT)); + g_variant_type_info_unref (s_child.type_info); return g_variant_new_tuple (NULL, 0); } -- GitLab From 5f4485c4ff57fdefb1661531788def7ca5a47328 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Thu, 27 Oct 2022 12:00:04 +0100 Subject: [PATCH 16/18] gvariant-serialiser: Check offset table entry size is minimal The entries in an offset table (which is used for variable sized arrays and tuples containing variable sized members) are sized so that they can address every byte in the overall variant. The specification requires that for a variant to be in normal form, its offset table entries must be the minimum width such that they can address every byte in the variant. That minimality requirement was not checked in `g_variant_is_normal_form()`, leading to two different byte arrays being interpreted as the normal form of a given variant tree. That kind of confusion could potentially be exploited, and is certainly a bug. Fix it by adding the necessary checks on offset table entry width, and unit tests. Spotted by William Manley. Signed-off-by: Philip Withnall Fixes: #2794 --- glib/gvariant-serialiser.c | 19 +++- glib/tests/gvariant.c | 176 +++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) diff --git a/glib/gvariant-serialiser.c b/glib/gvariant-serialiser.c index 69baeeb395..fadefab659 100644 --- a/glib/gvariant-serialiser.c +++ b/glib/gvariant-serialiser.c @@ -696,6 +696,10 @@ gvs_variable_sized_array_get_frame_offsets (GVariantSerialised value) out.data_size = last_end; out.array = value.data + last_end; out.length = offsets_array_size / out.offset_size; + + if (out.length > 0 && gvs_calculate_total_size (last_end, out.length) != value.size) + return out; /* offset size not minimal */ + out.is_normal = TRUE; return out; @@ -1207,6 +1211,7 @@ gvs_tuple_is_normal (GVariantSerialised value) gsize length; gsize offset; gsize i; + gsize offset_table_size; /* as per the comment in gvs_tuple_get_child() */ if G_UNLIKELY (value.data == NULL && value.size != 0) @@ -1311,7 +1316,19 @@ gvs_tuple_is_normal (GVariantSerialised value) } } - return offset_ptr == offset; + /* @offset_ptr has been counting backwards from the end of the variant, to + * find the beginning of the offset table. @offset has been counting forwards + * from the beginning of the variant to find the end of the data. They should + * have met in the middle. */ + if (offset_ptr != offset) + return FALSE; + + offset_table_size = value.size - offset_ptr; + if (value.size > 0 && + gvs_calculate_total_size (offset, offset_table_size / offset_size) != value.size) + return FALSE; /* offset size not minimal */ + + return TRUE; } /* Variants {{{2 diff --git a/glib/tests/gvariant.c b/glib/tests/gvariant.c index 18aeb80f12..d4893ed407 100644 --- a/glib/tests/gvariant.c +++ b/glib/tests/gvariant.c @@ -5234,6 +5234,86 @@ test_normal_checking_array_offsets2 (void) g_variant_unref (variant); } +/* Test that an otherwise-valid serialised GVariant is considered non-normal if + * its offset table entries are too wide. + * + * See §2.3.6 (Framing Offsets) of the GVariant specification. */ +static void +test_normal_checking_array_offsets_minimal_sized (void) +{ + GVariantBuilder builder; + gsize i; + GVariant *aay_constructed = NULL; + const guint8 *data = NULL; + guint8 *data_owned = NULL; + GVariant *aay_deserialised = NULL; + GVariant *aay_normalised = NULL; + + /* Construct an array of type aay, consisting of 128 elements which are each + * an empty array, i.e. `[[] * 128]`. This is chosen because the inner + * elements are variable sized (making the outer array variable sized, so it + * must have an offset table), but they are also zero-sized when serialised. + * So the serialised representation of @aay_constructed consists entirely of + * its offset table, which is entirely zeroes. + * + * The array is chosen to be 128 elements long because that means offset + * table entries which are 1 byte long. If the elements in the array were + * non-zero-sized (to the extent that the overall array is ≥256 bytes long), + * the offset table entries would end up being 2 bytes long. */ + g_variant_builder_init (&builder, G_VARIANT_TYPE ("aay")); + + for (i = 0; i < 128; i++) + g_variant_builder_add_value (&builder, g_variant_new_array (G_VARIANT_TYPE_BYTE, NULL, 0)); + + aay_constructed = g_variant_builder_end (&builder); + + /* Verify that the constructed array is in normal form, and its serialised + * form is `b'\0' * 128`. */ + g_assert_true (g_variant_is_normal_form (aay_constructed)); + g_assert_cmpuint (g_variant_n_children (aay_constructed), ==, 128); + g_assert_cmpuint (g_variant_get_size (aay_constructed), ==, 128); + + data = g_variant_get_data (aay_constructed); + for (i = 0; i < g_variant_get_size (aay_constructed); i++) + g_assert_cmpuint (data[i], ==, 0); + + /* Construct a serialised `aay` GVariant which is `b'\0' * 256`. This has to + * be a non-normal form of `[[] * 128]`, with 2-byte-long offset table + * entries, because each offset table entry has to be able to reference all of + * the byte boundaries in the container. All the entries in the offset table + * are zero, so all the elements of the array are zero-sized. */ + data = data_owned = g_malloc0 (256); + aay_deserialised = g_variant_new_from_data (G_VARIANT_TYPE ("aay"), + data, + 256, + FALSE, + g_free, + g_steal_pointer (&data_owned)); + + g_assert_false (g_variant_is_normal_form (aay_deserialised)); + g_assert_cmpuint (g_variant_n_children (aay_deserialised), ==, 128); + g_assert_cmpuint (g_variant_get_size (aay_deserialised), ==, 256); + + data = g_variant_get_data (aay_deserialised); + for (i = 0; i < g_variant_get_size (aay_deserialised); i++) + g_assert_cmpuint (data[i], ==, 0); + + /* Get its normal form. That should change the serialised size. */ + aay_normalised = g_variant_get_normal_form (aay_deserialised); + + g_assert_true (g_variant_is_normal_form (aay_normalised)); + g_assert_cmpuint (g_variant_n_children (aay_normalised), ==, 128); + g_assert_cmpuint (g_variant_get_size (aay_normalised), ==, 128); + + data = g_variant_get_data (aay_normalised); + for (i = 0; i < g_variant_get_size (aay_normalised); i++) + g_assert_cmpuint (data[i], ==, 0); + + g_variant_unref (aay_normalised); + g_variant_unref (aay_deserialised); + g_variant_unref (aay_constructed); +} + /* Test that a tuple with invalidly large values in its offset table is * normalised successfully without looping infinitely. */ static void @@ -5428,6 +5508,98 @@ test_normal_checking_tuple_offsets4 (void) g_variant_unref (variant); } +/* Test that an otherwise-valid serialised GVariant is considered non-normal if + * its offset table entries are too wide. + * + * See §2.3.6 (Framing Offsets) of the GVariant specification. */ +static void +test_normal_checking_tuple_offsets_minimal_sized (void) +{ + GString *type_string = NULL; + GVariantBuilder builder; + gsize i; + GVariant *ray_constructed = NULL; + const guint8 *data = NULL; + guint8 *data_owned = NULL; + GVariant *ray_deserialised = NULL; + GVariant *ray_normalised = NULL; + + /* Construct a tuple of type (ay…ay), consisting of 129 members which are each + * an empty array, i.e. `([] * 129)`. This is chosen because the inner + * members are variable sized, so the outer tuple must have an offset table, + * but they are also zero-sized when serialised. So the serialised + * representation of @ray_constructed consists entirely of its offset table, + * which is entirely zeroes. + * + * The tuple is chosen to be 129 members long because that means it has 128 + * offset table entries which are 1 byte long each. If the members in the + * tuple were non-zero-sized (to the extent that the overall tuple is ≥256 + * bytes long), the offset table entries would end up being 2 bytes long. + * + * 129 members are used unlike 128 array elements in + * test_normal_checking_array_offsets_minimal_sized(), because the last member + * in a tuple never needs an offset table entry. */ + type_string = g_string_new (""); + g_string_append_c (type_string, '('); + for (i = 0; i < 129; i++) + g_string_append (type_string, "ay"); + g_string_append_c (type_string, ')'); + + g_variant_builder_init (&builder, G_VARIANT_TYPE (type_string->str)); + + for (i = 0; i < 129; i++) + g_variant_builder_add_value (&builder, g_variant_new_array (G_VARIANT_TYPE_BYTE, NULL, 0)); + + ray_constructed = g_variant_builder_end (&builder); + + /* Verify that the constructed tuple is in normal form, and its serialised + * form is `b'\0' * 128`. */ + g_assert_true (g_variant_is_normal_form (ray_constructed)); + g_assert_cmpuint (g_variant_n_children (ray_constructed), ==, 129); + g_assert_cmpuint (g_variant_get_size (ray_constructed), ==, 128); + + data = g_variant_get_data (ray_constructed); + for (i = 0; i < g_variant_get_size (ray_constructed); i++) + g_assert_cmpuint (data[i], ==, 0); + + /* Construct a serialised `(ay…ay)` GVariant which is `b'\0' * 256`. This has + * to be a non-normal form of `([] * 129)`, with 2-byte-long offset table + * entries, because each offset table entry has to be able to reference all of + * the byte boundaries in the container. All the entries in the offset table + * are zero, so all the members of the tuple are zero-sized. */ + data = data_owned = g_malloc0 (256); + ray_deserialised = g_variant_new_from_data (G_VARIANT_TYPE (type_string->str), + data, + 256, + FALSE, + g_free, + g_steal_pointer (&data_owned)); + + g_assert_false (g_variant_is_normal_form (ray_deserialised)); + g_assert_cmpuint (g_variant_n_children (ray_deserialised), ==, 129); + g_assert_cmpuint (g_variant_get_size (ray_deserialised), ==, 256); + + data = g_variant_get_data (ray_deserialised); + for (i = 0; i < g_variant_get_size (ray_deserialised); i++) + g_assert_cmpuint (data[i], ==, 0); + + /* Get its normal form. That should change the serialised size. */ + ray_normalised = g_variant_get_normal_form (ray_deserialised); + + g_assert_true (g_variant_is_normal_form (ray_normalised)); + g_assert_cmpuint (g_variant_n_children (ray_normalised), ==, 129); + g_assert_cmpuint (g_variant_get_size (ray_normalised), ==, 128); + + data = g_variant_get_data (ray_normalised); + for (i = 0; i < g_variant_get_size (ray_normalised); i++) + g_assert_cmpuint (data[i], ==, 0); + + g_variant_unref (ray_normalised); + g_variant_unref (ray_deserialised); + g_variant_unref (ray_constructed); + g_string_free (type_string, TRUE); +} + /* Test that an empty object path is normalised successfully to the base object * path, ‘/’. */ static void @@ -5576,6 +5748,8 @@ main (int argc, char **argv) test_normal_checking_array_offsets); g_test_add_func ("/gvariant/normal-checking/array-offsets2", test_normal_checking_array_offsets2); + g_test_add_func ("/gvariant/normal-checking/array-offsets/minimal-sized", + test_normal_checking_array_offsets_minimal_sized); g_test_add_func ("/gvariant/normal-checking/tuple-offsets", test_normal_checking_tuple_offsets); g_test_add_func ("/gvariant/normal-checking/tuple-offsets2", @@ -5584,6 +5758,8 @@ main (int argc, char **argv) test_normal_checking_tuple_offsets3); g_test_add_func ("/gvariant/normal-checking/tuple-offsets4", test_normal_checking_tuple_offsets4); + g_test_add_func ("/gvariant/normal-checking/tuple-offsets/minimal-sized", + test_normal_checking_tuple_offsets_minimal_sized); g_test_add_func ("/gvariant/normal-checking/empty-object-path", test_normal_checking_empty_object_path); -- GitLab From 4c4cf568f0f710baf0bd04d52df715636bc6b971 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Thu, 27 Oct 2022 16:13:54 +0100 Subject: [PATCH 17/18] gvariant: Fix g_variant_byteswap() returning non-normal data sometimes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If `g_variant_byteswap()` was called on a non-normal variant of a type which doesn’t need byteswapping, it would return a non-normal output. That contradicts the documentation, which says that the return value is always in normal form. Fix the code so it matches the documentation. Includes a unit test. Signed-off-by: Philip Withnall Helps: #2797 --- glib/gvariant.c | 8 +++++--- glib/tests/gvariant.c | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/glib/gvariant.c b/glib/gvariant.c index 356e199a0f..f7b2e97036 100644 --- a/glib/gvariant.c +++ b/glib/gvariant.c @@ -6084,14 +6084,16 @@ g_variant_byteswap (GVariant *value) g_variant_serialised_byteswap (serialised); bytes = g_bytes_new_take (serialised.data, serialised.size); - new = g_variant_new_from_bytes (g_variant_get_type (value), bytes, TRUE); + new = g_variant_ref_sink (g_variant_new_from_bytes (g_variant_get_type (value), bytes, TRUE)); g_bytes_unref (bytes); } else /* contains no multi-byte data */ - new = value; + new = g_variant_get_normal_form (value); - return g_variant_ref_sink (new); + g_assert (g_variant_is_trusted (new)); + + return g_steal_pointer (&new); } /** diff --git a/glib/tests/gvariant.c b/glib/tests/gvariant.c index d4893ed407..1e7ed16cc9 100644 --- a/glib/tests/gvariant.c +++ b/glib/tests/gvariant.c @@ -3826,6 +3826,29 @@ test_gv_byteswap (void) g_free (string); } +static void +test_gv_byteswap_non_normal_non_aligned (void) +{ + const guint8 data[] = { 0x02 }; + GVariant *v = NULL; + GVariant *v_byteswapped = NULL; + + g_test_summary ("Test that calling g_variant_byteswap() on a variant which " + "is in non-normal form and doesn’t need byteswapping returns " + "the same variant in normal form."); + + v = g_variant_new_from_data (G_VARIANT_TYPE_BOOLEAN, data, sizeof (data), FALSE, NULL, NULL); + g_assert_false (g_variant_is_normal_form (v)); + + v_byteswapped = g_variant_byteswap (v); + g_assert_true (g_variant_is_normal_form (v_byteswapped)); + + g_assert_cmpvariant (v, v_byteswapped); + + g_variant_unref (v); + g_variant_unref (v_byteswapped); +} + static void test_parser (void) { @@ -5711,6 +5734,7 @@ main (int argc, char **argv) g_test_add_func ("/gvariant/builder-memory", test_builder_memory); g_test_add_func ("/gvariant/hashing", test_hashing); g_test_add_func ("/gvariant/byteswap", test_gv_byteswap); + g_test_add_func ("/gvariant/byteswap/non-normal-non-aligned", test_gv_byteswap_non_normal_non_aligned); g_test_add_func ("/gvariant/parser", test_parses); g_test_add_func ("/gvariant/parser/integer-bounds", test_parser_integer_bounds); g_test_add_func ("/gvariant/parser/recursion", test_parser_recursion); -- GitLab From a70a16b28bf78403dc86ae798c291ba167573d4a Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Thu, 27 Oct 2022 22:53:13 +0100 Subject: [PATCH 18/18] gvariant: Allow g_variant_byteswap() to operate on tree-form variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids needing to always serialise a variant before byteswapping it. With variants in non-normal forms, serialisation can result in a large increase in size of the variant, and a lot of allocations for leaf `GVariant`s. This can lead to a denial of service attack. Avoid that by changing byteswapping so that it happens on the tree form of the variant if the input is in non-normal form. If the input is in normal form (either serialised or in tree form), continue using the existing code as byteswapping an already-serialised normal variant is about 3× faster than byteswapping on the equivalent tree form. The existing unit tests cover byteswapping well, but need some adaptation so that they operate on tree form variants too. I considered dropping the serialised byteswapping code and doing all byteswapping on tree-form variants, as that would make maintenance simpler (avoiding having two parallel implementations of byteswapping). However, most inputs to `g_variant_byteswap()` are likely to be serialised variants (coming from a byte array of input from some foreign source) and most of them are going to be in normal form (as corruption and malicious action are rare). So getting rid of the serialised byteswapping code would impose quite a performance penalty on the common case. Signed-off-by: Philip Withnall Fixes: #2797 --- glib/gvariant.c | 87 ++++++++++++++++++++++++++++++++----------- glib/tests/gvariant.c | 57 ++++++++++++++++++++++++---- 2 files changed, 115 insertions(+), 29 deletions(-) diff --git a/glib/gvariant.c b/glib/gvariant.c index f7b2e97036..5fef88d58d 100644 --- a/glib/gvariant.c +++ b/glib/gvariant.c @@ -5852,7 +5852,8 @@ g_variant_iter_loop (GVariantIter *iter, /* Serialized data {{{1 */ static GVariant * -g_variant_deep_copy (GVariant *value) +g_variant_deep_copy (GVariant *value, + gboolean byteswap) { switch (g_variant_classify (value)) { @@ -5869,7 +5870,7 @@ g_variant_deep_copy (GVariant *value) for (i = 0, n_children = g_variant_n_children (value); i < n_children; i++) { GVariant *child = g_variant_get_child_value (value, i); - g_variant_builder_add_value (&builder, g_variant_deep_copy (child)); + g_variant_builder_add_value (&builder, g_variant_deep_copy (child, byteswap)); g_variant_unref (child); } @@ -5920,7 +5921,7 @@ g_variant_deep_copy (GVariant *value) * be non-normal for reasons other than invalid offset table * entries. As they are all the same type, they will all have * the same default value though, so keep that around. */ - g_variant_builder_add_value (&builder, g_variant_deep_copy (child)); + g_variant_builder_add_value (&builder, g_variant_deep_copy (child, byteswap)); } else if (child == NULL && first_invalid_child_deep_copy != NULL) { @@ -5929,7 +5930,7 @@ g_variant_deep_copy (GVariant *value) else if (child == NULL) { child = g_variant_get_child_value (value, i); - first_invalid_child_deep_copy = g_variant_ref_sink (g_variant_deep_copy (child)); + first_invalid_child_deep_copy = g_variant_ref_sink (g_variant_deep_copy (child, byteswap)); g_variant_builder_add_value (&builder, first_invalid_child_deep_copy); } @@ -5948,28 +5949,63 @@ g_variant_deep_copy (GVariant *value) return g_variant_new_byte (g_variant_get_byte (value)); case G_VARIANT_CLASS_INT16: - return g_variant_new_int16 (g_variant_get_int16 (value)); + if (byteswap) + return g_variant_new_int16 (GUINT16_SWAP_LE_BE (g_variant_get_int16 (value))); + else + return g_variant_new_int16 (g_variant_get_int16 (value)); case G_VARIANT_CLASS_UINT16: - return g_variant_new_uint16 (g_variant_get_uint16 (value)); + if (byteswap) + return g_variant_new_uint16 (GUINT16_SWAP_LE_BE (g_variant_get_uint16 (value))); + else + return g_variant_new_uint16 (g_variant_get_uint16 (value)); case G_VARIANT_CLASS_INT32: - return g_variant_new_int32 (g_variant_get_int32 (value)); + if (byteswap) + return g_variant_new_int32 (GUINT32_SWAP_LE_BE (g_variant_get_int32 (value))); + else + return g_variant_new_int32 (g_variant_get_int32 (value)); case G_VARIANT_CLASS_UINT32: - return g_variant_new_uint32 (g_variant_get_uint32 (value)); + if (byteswap) + return g_variant_new_uint32 (GUINT32_SWAP_LE_BE (g_variant_get_uint32 (value))); + else + return g_variant_new_uint32 (g_variant_get_uint32 (value)); case G_VARIANT_CLASS_INT64: - return g_variant_new_int64 (g_variant_get_int64 (value)); + if (byteswap) + return g_variant_new_int64 (GUINT64_SWAP_LE_BE (g_variant_get_int64 (value))); + else + return g_variant_new_int64 (g_variant_get_int64 (value)); case G_VARIANT_CLASS_UINT64: - return g_variant_new_uint64 (g_variant_get_uint64 (value)); + if (byteswap) + return g_variant_new_uint64 (GUINT64_SWAP_LE_BE (g_variant_get_uint64 (value))); + else + return g_variant_new_uint64 (g_variant_get_uint64 (value)); case G_VARIANT_CLASS_HANDLE: - return g_variant_new_handle (g_variant_get_handle (value)); + if (byteswap) + return g_variant_new_handle (GUINT32_SWAP_LE_BE (g_variant_get_handle (value))); + else + return g_variant_new_handle (g_variant_get_handle (value)); case G_VARIANT_CLASS_DOUBLE: - return g_variant_new_double (g_variant_get_double (value)); + if (byteswap) + { + /* We have to convert the double to a uint64 here using a union, + * because a cast will round it numerically. */ + union + { + guint64 u64; + gdouble dbl; + } u1, u2; + u1.dbl = g_variant_get_double (value); + u2.u64 = GUINT64_SWAP_LE_BE (u1.u64); + return g_variant_new_double (u2.dbl); + } + else + return g_variant_new_double (g_variant_get_double (value)); case G_VARIANT_CLASS_STRING: return g_variant_new_string (g_variant_get_string (value, NULL)); @@ -6026,7 +6062,7 @@ g_variant_get_normal_form (GVariant *value) if (g_variant_is_normal_form (value)) return g_variant_ref (value); - trusted = g_variant_deep_copy (value); + trusted = g_variant_deep_copy (value, FALSE); g_assert (g_variant_is_trusted (trusted)); return g_variant_ref_sink (trusted); @@ -6046,6 +6082,11 @@ g_variant_get_normal_form (GVariant *value) * contain multi-byte numeric data. That include strings, booleans, * bytes and containers containing only these things (recursively). * + * While this function can safely handle untrusted, non-normal data, it is + * recommended to check whether the input is in normal form beforehand, using + * g_variant_is_normal_form(), and to reject non-normal inputs if your + * application can be strict about what inputs it rejects. + * * The returned value is always in normal form and is marked as trusted. * * Returns: (transfer full): the byteswapped form of @value @@ -6064,22 +6105,21 @@ g_variant_byteswap (GVariant *value) g_variant_type_info_query (type_info, &alignment, NULL); - if (alignment) - /* (potentially) contains multi-byte numeric data */ + if (alignment && g_variant_is_normal_form (value)) { + /* (potentially) contains multi-byte numeric data, but is also already in + * normal form so we can use a faster byteswapping codepath on the + * serialised data */ GVariantSerialised serialised = { 0, }; - GVariant *trusted; GBytes *bytes; - trusted = g_variant_get_normal_form (value); - serialised.type_info = g_variant_get_type_info (trusted); - serialised.size = g_variant_get_size (trusted); + serialised.type_info = g_variant_get_type_info (value); + serialised.size = g_variant_get_size (value); serialised.data = g_malloc (serialised.size); - serialised.depth = g_variant_get_depth (trusted); + serialised.depth = g_variant_get_depth (value); serialised.ordered_offsets_up_to = G_MAXSIZE; /* operating on the normal form */ serialised.checked_offsets_up_to = G_MAXSIZE; - g_variant_store (trusted, serialised.data); - g_variant_unref (trusted); + g_variant_store (value, serialised.data); g_variant_serialised_byteswap (serialised); @@ -6087,6 +6127,9 @@ g_variant_byteswap (GVariant *value) new = g_variant_ref_sink (g_variant_new_from_bytes (g_variant_get_type (value), bytes, TRUE)); g_bytes_unref (bytes); } + else if (alignment) + /* (potentially) contains multi-byte numeric data */ + new = g_variant_ref_sink (g_variant_deep_copy (value, TRUE)); else /* contains no multi-byte data */ new = g_variant_get_normal_form (value); diff --git a/glib/tests/gvariant.c b/glib/tests/gvariant.c index 1e7ed16cc9..3357488a4f 100644 --- a/glib/tests/gvariant.c +++ b/glib/tests/gvariant.c @@ -2288,24 +2288,67 @@ serialise_tree (TreeInstance *tree, static void test_byteswap (void) { - GVariantSerialised one = { 0, }, two = { 0, }; + GVariantSerialised one = { 0, }, two = { 0, }, three = { 0, }; TreeInstance *tree; - + GVariant *one_variant = NULL; + GVariant *two_variant = NULL; + GVariant *two_byteswapped = NULL; + GVariant *three_variant = NULL; + GVariant *three_byteswapped = NULL; + guint8 *three_data_copy = NULL; + gsize three_size_copy = 0; + + /* Write a tree out twice, once normally and once byteswapped. */ tree = tree_instance_new (NULL, 3); serialise_tree (tree, &one); + one_variant = g_variant_new_from_data (G_VARIANT_TYPE (g_variant_type_info_get_type_string (one.type_info)), + one.data, one.size, FALSE, NULL, NULL); + i_am_writing_byteswapped = TRUE; serialise_tree (tree, &two); + serialise_tree (tree, &three); i_am_writing_byteswapped = FALSE; - g_variant_serialised_byteswap (two); - - g_assert_cmpmem (one.data, one.size, two.data, two.size); - g_assert_cmpuint (one.depth, ==, two.depth); - + /* Swap the first byteswapped one back using the function we want to test. */ + two_variant = g_variant_new_from_data (G_VARIANT_TYPE (g_variant_type_info_get_type_string (two.type_info)), + two.data, two.size, FALSE, NULL, NULL); + two_byteswapped = g_variant_byteswap (two_variant); + + /* Make the second byteswapped one non-normal (hopefully), and then byteswap + * it back using the function we want to test in its non-normal mode. + * This might not work because it’s not necessarily possible to make an + * arbitrary random variant non-normal. Adding a single zero byte to the end + * often makes something non-normal but still readable. */ + three_size_copy = three.size + 1; + three_data_copy = g_malloc (three_size_copy); + memcpy (three_data_copy, three.data, three.size); + three_data_copy[three.size] = '\0'; + + three_variant = g_variant_new_from_data (G_VARIANT_TYPE (g_variant_type_info_get_type_string (three.type_info)), + three_data_copy, three_size_copy, FALSE, NULL, NULL); + three_byteswapped = g_variant_byteswap (three_variant); + + /* Check they’re the same. We can always compare @one_variant and + * @two_byteswapped. We can only compare @two_byteswapped and + * @three_byteswapped if @two_variant and @three_variant are equal: in that + * case, the corruption to @three_variant was enough to make it non-normal but + * not enough to change its value. */ + g_assert_cmpvariant (one_variant, two_byteswapped); + + if (g_variant_equal (two_variant, three_variant)) + g_assert_cmpvariant (two_byteswapped, three_byteswapped); + + g_variant_unref (three_byteswapped); + g_variant_unref (three_variant); + g_variant_unref (two_byteswapped); + g_variant_unref (two_variant); + g_variant_unref (one_variant); tree_instance_free (tree); g_free (one.data); g_free (two.data); + g_free (three.data); + g_free (three_data_copy); } static void -- GitLab