glib2/backport-CVE-2023-24593_CVE-2023-25180-1.patch
han_hui_hui df275b6ea3 fix CVE-2023-24593 and CVE-2023-25180
(cherry picked from commit a4ad6af44c42a3cef3b6e6f91f6db5e43d2a1dae)
2023-04-04 14:17:00 +08:00

2975 lines
110 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

From 1deacdd4e8e35a5cf1417918ca4f6b0afa6409b1 Mon Sep 17 00:00:00 2001
From: William Manley <will@stb-tester.com>
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 <will@stb-tester.com>
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 <will@stb-tester.com>
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 (&current, 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 theyre
+ * 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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 theyre needed.
Signed-off-by: Philip Withnall <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 doesnt 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 theyre
+ * 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
+ * dont. 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 theyre 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 doesnt allow
+ * this as its 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 theyre 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 doesnt allow
+ * this as its 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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 theyre
- * normal (in order, no overlapping array elements). */
- if (index_ > value.ordered_offsets_up_to)
+ * normal (in order, no overlapping array elements).
+ *
+ * Dont bother checking if the highest known-good offset is lower than the
+ * highest checked offset, as that means theres an invalid element at that
+ * index, so theres 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
* dont. 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 <withnall@endlessm.com>
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 <withnall@endlessm.com>
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 <pwithnall@endlessos.org>
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`. Its 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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
---
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 dont 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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
---
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 doesnt 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 <pwithnall@endlessos.org>
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 doesnt 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 <pwithnall@endlessos.org>
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 <pwithnall@endlessos.org>
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 its 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 theyre 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