From 5cdacced3fe58965d1ff592bd8fc3c100accede4 Mon Sep 17 00:00:00 2001 From: Patrick Griffis Date: Fri, 18 Mar 2022 14:22:38 +0000 Subject: [PATCH 01/12] tests: Add basic test framework for GResolver DNS parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split out from https://gitlab.gnome.org/GNOME/glib/-/merge_requests/2134 by Philip Withnall so it can be used in advance of HTTPS DNS record support landing. Reworked to no longer use test fixtures, as it’s simple enough to build the response header in each test. The tests are built on Unix only, as they test the parsing code in `g_resolver_records_from_res_query()`, which is Unix-specific. The Windows DNS APIs provide much more structured results which don’t need parsing. --- gio/meson.build | 8 +++ gio/tests/meson.build | 1 + gio/tests/resolver-parsing.c | 116 +++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 gio/tests/resolver-parsing.c diff --git a/gio/meson.build b/gio/meson.build index 874de4b15..543763eea 100644 --- a/gio/meson.build +++ b/gio/meson.build @@ -74,6 +74,14 @@ if host_system != 'windows' endif endif + # dn_comp() + if cc.links('''#include + int main (int argc, char ** argv) { + return dn_comp(NULL, NULL, 0, NULL, NULL) == -1; + } ''', args : network_args, name : 'dn_comp()') + glib_conf.set('HAVE_DN_COMP', 1) + endif + # res_nclose() if cc.links('''#include #include diff --git a/gio/tests/meson.build b/gio/tests/meson.build index a27bc53d1..3ed23a5f2 100644 --- a/gio/tests/meson.build +++ b/gio/tests/meson.build @@ -191,6 +191,7 @@ if host_machine.system() != 'windows' }, 'gdbus-peer-object-manager' : {}, 'live-g-file' : {}, + 'resolver-parsing' : {'dependencies' : [network_libs]}, 'socket-address' : {}, 'stream-rw_all' : {}, 'unix-fd' : {}, diff --git a/gio/tests/resolver-parsing.c b/gio/tests/resolver-parsing.c new file mode 100644 index 000000000..e6fc74936 --- /dev/null +++ b/gio/tests/resolver-parsing.c @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + * + * Authors: Patrick Griffis + */ + +#include "config.h" + +#include +#include + +#define GIO_COMPILATION +#include "gthreadedresolver.h" +#undef GIO_COMPILATION + +#ifdef HAVE_DN_COMP +static void +dns_builder_add_uint8 (GByteArray *builder, + guint8 value) +{ + g_byte_array_append (builder, &value, 1); +} + +static void +dns_builder_add_uint16 (GByteArray *builder, + guint16 value) +{ + dns_builder_add_uint8 (builder, (value >> 8) & 0xFF); + dns_builder_add_uint8 (builder, (value) & 0xFF); +} + +static void +dns_builder_add_uint32 (GByteArray *builder, + guint32 value) +{ + dns_builder_add_uint8 (builder, (value >> 24) & 0xFF); + dns_builder_add_uint8 (builder, (value >> 16) & 0xFF); + dns_builder_add_uint8 (builder, (value >> 8) & 0xFF); + dns_builder_add_uint8 (builder, (value) & 0xFF); +} + +static void +dns_builder_add_length_prefixed_string (GByteArray *builder, + const char *string) +{ + guint8 length; + + g_assert (strlen (string) <= G_MAXUINT8); + + length = (guint8) strlen (string); + dns_builder_add_uint8 (builder, length); + + /* Don't include trailing NUL */ + g_byte_array_append (builder, (const guchar *)string, length); +} + +static void +dns_builder_add_domain (GByteArray *builder, + const char *string) +{ + int ret; + guchar buffer[256]; + + ret = dn_comp (string, buffer, sizeof (buffer), NULL, NULL); + g_assert (ret != -1); + + g_byte_array_append (builder, buffer, ret); +} + +static void +dns_builder_add_answer_data (GByteArray *builder, + GByteArray *answer) +{ + dns_builder_add_uint16 (builder, answer->len); /* rdlength */ + g_byte_array_append (builder, answer->data, answer->len); +} + +static GByteArray * +dns_header (void) +{ + GByteArray *answer = g_byte_array_sized_new (2046); + + /* Start with a header, we ignore everything except ancount. + https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1 */ + dns_builder_add_uint16 (answer, 0); /* ID */ + dns_builder_add_uint16 (answer, 0); /* |QR| Opcode |AA|TC|RD|RA| Z | RCODE | */ + dns_builder_add_uint16 (answer, 0); /* QDCOUNT */ + dns_builder_add_uint16 (answer, 1); /* ANCOUNT (1 answer) */ + dns_builder_add_uint16 (answer, 0); /* NSCOUNT */ + dns_builder_add_uint16 (answer, 0); /* ARCOUNT */ + + return g_steal_pointer (&answer); +} +#endif /* HAVE_DN_COMP */ + +int +main (int argc, + char *argv[]) +{ + g_test_init (&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL); + + return g_test_run (); +} From 263ca69da80eab0a2b8e3308a562675a8c643988 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 18 Mar 2022 15:49:33 +0000 Subject: [PATCH 02/12] gthreadedresolver: Check header length when parsing response Otherwise we could read off the end of an invalid response. oss-fuzz#42538 Signed-off-by: Philip Withnall --- gio/gthreadedresolver.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/gio/gthreadedresolver.c b/gio/gthreadedresolver.c index 48545d6ad..3caa9f36e 100644 --- a/gio/gthreadedresolver.c +++ b/gio/gthreadedresolver.c @@ -667,6 +667,7 @@ g_resolver_records_from_res_query (const gchar *rrname, const HEADER *header; GList *records; GVariant *record; + gsize len_unsigned; if (len <= 0) { @@ -689,11 +690,23 @@ g_resolver_records_from_res_query (const gchar *rrname, return NULL; } + /* We know len ≥ 0 now. */ + len_unsigned = (gsize) len; + + if (len_unsigned < sizeof (HEADER)) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the first placeholder is a domain name, the + * second is an error message */ + _("Error resolving “%s”: %s"), rrname, _("Malformed DNS packet")); + return NULL; + } + records = NULL; header = (HEADER *)answer; p = answer + sizeof (HEADER); - end = answer + len; + end = answer + len_unsigned; /* Skip query */ count = ntohs (header->qdcount); From 8b73d7bbf9b2184e063edc649c48e726873491d0 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 18 Mar 2022 15:51:20 +0000 Subject: [PATCH 03/12] gthreadedresolver: Handle error returns from dn_expand() in headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is possible for `dn_expand()` to fail; if so, it’ll return `-1`, which will mess up subsequent parsing. Signed-off-by: Philip Withnall --- gio/gthreadedresolver.c | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/gio/gthreadedresolver.c b/gio/gthreadedresolver.c index 3caa9f36e..2ae354bc5 100644 --- a/gio/gthreadedresolver.c +++ b/gio/gthreadedresolver.c @@ -712,8 +712,22 @@ g_resolver_records_from_res_query (const gchar *rrname, count = ntohs (header->qdcount); while (count-- && p < end) { - p += dn_expand (answer, end, p, namebuf, sizeof (namebuf)); - p += 4; + int expand_result; + + expand_result = dn_expand (answer, end, p, namebuf, sizeof (namebuf)); + if (expand_result < 0 || end - p < expand_result + 4) + { + /* Not possible to recover parsing as the length of the rest of the + * record is unknown or is too short. */ + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the first placeholder is a domain name, the + * second is an error message */ + _("Error resolving “%s”: %s"), rrname, _("Malformed DNS packet")); + return NULL; + } + + p += expand_result; + p += 4; /* skip TYPE and CLASS */ /* To silence gcc warnings */ namebuf[0] = namebuf[1]; @@ -723,7 +737,20 @@ g_resolver_records_from_res_query (const gchar *rrname, count = ntohs (header->ancount); while (count-- && p < end) { - p += dn_expand (answer, end, p, namebuf, sizeof (namebuf)); + int expand_result; + + expand_result = dn_expand (answer, end, p, namebuf, sizeof (namebuf)); + if (expand_result < 0 || end - p < expand_result + 10) + { + /* Not possible to recover parsing as the length of the rest of the + * record is unknown or is too short. */ + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + _("Error resolving “%s”"), rrname); + g_list_free_full (records, (GDestroyNotify) g_variant_unref); + return NULL; + } + + p += expand_result; GETSHORT (type, p); GETSHORT (qclass, p); p += 4; /* ignore the ttl (type=long) value */ From 2a7b4db243188f4500947138bbede1cf5d9110ef Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 21 Mar 2022 17:45:17 +0000 Subject: [PATCH 04/12] gthreadedresolver: Expose g_resolver_record_type_to_rrtype() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So that it can be used in the tests. It’s not part of the public, documented, supported API. Signed-off-by: Philip Withnall --- gio/gthreadedresolver.c | 2 +- gio/gthreadedresolver.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gio/gthreadedresolver.c b/gio/gthreadedresolver.c index 2ae354bc5..59f229a16 100644 --- a/gio/gthreadedresolver.c +++ b/gio/gthreadedresolver.c @@ -633,7 +633,7 @@ parse_res_txt (const guint8 *answer, return record; } -static gint +gint g_resolver_record_type_to_rrtype (GResolverRecordType type) { switch (type) diff --git a/gio/gthreadedresolver.h b/gio/gthreadedresolver.h index 95a5fe55f..c4d6dc28d 100644 --- a/gio/gthreadedresolver.h +++ b/gio/gthreadedresolver.h @@ -50,6 +50,8 @@ GList *g_resolver_records_from_res_query (const gchar *rrname, gssize len, gint herr, GError **error); +GLIB_AVAILABLE_IN_ALL +gint g_resolver_record_type_to_rrtype (GResolverRecordType type); G_END_DECLS From 51f70fe62ec6023d5a28f9283997ff47da713865 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 18 Mar 2022 15:54:19 +0000 Subject: [PATCH 05/12] tests: Add tests for invalid DNS response header parsing Signed-off-by: Philip Withnall --- gio/tests/resolver-parsing.c | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/gio/tests/resolver-parsing.c b/gio/tests/resolver-parsing.c index e6fc74936..bf1ba1b73 100644 --- a/gio/tests/resolver-parsing.c +++ b/gio/tests/resolver-parsing.c @@ -106,11 +106,53 @@ dns_header (void) } #endif /* HAVE_DN_COMP */ +static void +test_invalid_header (void) +{ + const struct + { + const guint8 *answer; + gsize answer_len; + GResolverError expected_error_code; + } + vectors[] = + { + /* No answer: */ + { (const guint8 *) "", 0, G_RESOLVER_ERROR_NOT_FOUND }, + /* Definitely too short to be a valid header: */ + { (const guint8 *) "\x20", 1, G_RESOLVER_ERROR_INTERNAL }, + /* One byte too short to be a valid header: */ + { (const guint8 *) "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 11, G_RESOLVER_ERROR_INTERNAL }, + /* Valid header indicating no answers: */ + { (const guint8 *) "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 12, G_RESOLVER_ERROR_NOT_FOUND }, + }; + gsize i; + + for (i = 0; i < G_N_ELEMENTS (vectors); i++) + { + GList *records = NULL; + GError *local_error = NULL; + + records = g_resolver_records_from_res_query ("example.org", + g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_NS), + vectors[i].answer, + vectors[i].answer_len, + 0, + &local_error); + + g_assert_error (local_error, G_RESOLVER_ERROR, (gint) vectors[i].expected_error_code); + g_assert_null (records); + g_clear_error (&local_error); + } +} + int main (int argc, char *argv[]) { g_test_init (&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL); + g_test_add_func ("/gresolver/invalid-header", test_invalid_header); + return g_test_run (); } From 81193c5aac04059455dc24aa573b06e1e773d700 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 18 Mar 2022 15:52:18 +0000 Subject: [PATCH 06/12] =?UTF-8?q?gthreadedresolver:=20Don=E2=80=99t=20warn?= =?UTF-8?q?=20on=20unrecognised=20record=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Otherwise the code isn’t forwards-compatible, and may be DOSed by servers returning unknown records, if `G_DEBUG=fatal-warnings` is enabled for some reason. Signed-off-by: Philip Withnall --- gio/gthreadedresolver.c | 2 +- gio/tests/resolver-parsing.c | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/gio/gthreadedresolver.c b/gio/gthreadedresolver.c index 59f229a16..568210203 100644 --- a/gio/gthreadedresolver.c +++ b/gio/gthreadedresolver.c @@ -780,7 +780,7 @@ g_resolver_records_from_res_query (const gchar *rrname, record = parse_res_txt (answer, p + rdlength, &p); break; default: - g_warn_if_reached (); + g_debug ("Unrecognised DNS record type %u", rrtype); record = NULL; break; } diff --git a/gio/tests/resolver-parsing.c b/gio/tests/resolver-parsing.c index bf1ba1b73..909917be8 100644 --- a/gio/tests/resolver-parsing.c +++ b/gio/tests/resolver-parsing.c @@ -146,6 +146,42 @@ test_invalid_header (void) } } +static void +test_unknown_record_type (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL; + GList *records = NULL; + GError *local_error = NULL; + const guint type_id = 20; /* ISDN, not supported anywhere */ + + /* An answer with an unsupported type chosen from + * https://en.wikipedia.org/wiki/List_of_DNS_record_types#[1]_Obsolete_record_types */ + answer = dns_header (); + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, type_id); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + dns_builder_add_uint16 (answer, 0); /* rdlength */ + + records = g_resolver_records_from_res_query ("example.org", + type_id, + answer->data, + answer->len, + 0, + &local_error); + + g_assert_error (local_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND); + g_assert_null (records); + g_clear_error (&local_error); + + g_byte_array_free (answer, TRUE); +#endif +} + int main (int argc, char *argv[]) @@ -153,6 +189,7 @@ main (int argc, g_test_init (&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL); g_test_add_func ("/gresolver/invalid-header", test_invalid_header); + g_test_add_func ("/gresolver/unknown-record-type", test_unknown_record_type); return g_test_run (); } From 023fab80f946d74cff931027168627fdb08742d4 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 18 Mar 2022 15:53:18 +0000 Subject: [PATCH 07/12] gthreadedresolver: Add error checking to all record parsing This should catch all kinds of invalid records, and correctly report them as errors. Heavily based on work by Patrick Griffis in !2134. Signed-off-by: Philip Withnall --- gio/gthreadedresolver.c | 131 +++++++++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 21 deletions(-) diff --git a/gio/gthreadedresolver.c b/gio/gthreadedresolver.c index 568210203..63a852d59 100644 --- a/gio/gthreadedresolver.c +++ b/gio/gthreadedresolver.c @@ -529,18 +529,56 @@ typedef enum __ns_type { #endif /* __BIONIC__ */ +/* Wrapper around dn_expand() which does associated length checks and returns + * errors as #GError. */ +static gboolean +expand_name (const gchar *rrname, + const guint8 *answer, + const guint8 *end, + const guint8 **p, + gchar *namebuf, + gsize namebuf_len, + GError **error) +{ + int expand_result; + + expand_result = dn_expand (answer, end, *p, namebuf, namebuf_len); + if (expand_result < 0 || end - *p < expand_result) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), rrname); + return FALSE; + } + + *p += expand_result; + + return TRUE; +} + static GVariant * parse_res_srv (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { gchar namebuf[1024]; guint16 priority, weight, port; + if (end - *p < 6) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "SRV"); + return NULL; + } + GETSHORT (priority, *p); GETSHORT (weight, *p); GETSHORT (port, *p); - *p += dn_expand (answer, end, *p, namebuf, sizeof (namebuf)); + + if (!expand_name ("SRV", answer, end, p, namebuf, sizeof (namebuf), error)) + return NULL; return g_variant_new ("(qqqs)", priority, @@ -552,14 +590,26 @@ parse_res_srv (const guint8 *answer, static GVariant * parse_res_soa (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { gchar mnamebuf[1024]; gchar rnamebuf[1024]; guint32 serial, refresh, retry, expire, ttl; - *p += dn_expand (answer, end, *p, mnamebuf, sizeof (mnamebuf)); - *p += dn_expand (answer, end, *p, rnamebuf, sizeof (rnamebuf)); + if (!expand_name ("SOA", answer, end, p, mnamebuf, sizeof (mnamebuf), error)) + return NULL; + + if (!expand_name ("SOA", answer, end, p, rnamebuf, sizeof (rnamebuf), error)) + return NULL; + + if (end - *p < 20) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "SOA"); + return NULL; + } GETLONG (serial, *p); GETLONG (refresh, *p); @@ -580,11 +630,13 @@ parse_res_soa (const guint8 *answer, static GVariant * parse_res_ns (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { gchar namebuf[1024]; - *p += dn_expand (answer, end, *p, namebuf, sizeof (namebuf)); + if (!expand_name ("NS", answer, end, p, namebuf, sizeof (namebuf), error)) + return NULL; return g_variant_new ("(s)", namebuf); } @@ -592,14 +644,24 @@ parse_res_ns (const guint8 *answer, static GVariant * parse_res_mx (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { gchar namebuf[1024]; guint16 preference; + if (end - *p < 2) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "MX"); + return NULL; + } + GETSHORT (preference, *p); - *p += dn_expand (answer, end, *p, namebuf, sizeof (namebuf)); + if (!expand_name ("MX", answer, end, p, namebuf, sizeof (namebuf), error)) + return NULL; return g_variant_new ("(qs)", preference, @@ -609,19 +671,35 @@ parse_res_mx (const guint8 *answer, static GVariant * parse_res_txt (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { GVariant *record; GPtrArray *array; const guint8 *at = *p; gsize len; + if (end - *p == 0) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "TXT"); + return NULL; + } + array = g_ptr_array_new_with_free_func (g_free); while (at < end) { len = *(at++); if (len > (gsize) (end - at)) - break; + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "TXT"); + g_ptr_array_free (array, TRUE); + return NULL; + } + g_ptr_array_add (array, g_strndup ((gchar *)at, len)); at += len; } @@ -668,6 +746,7 @@ g_resolver_records_from_res_query (const gchar *rrname, GList *records; GVariant *record; gsize len_unsigned; + GError *parsing_error = NULL; if (len <= 0) { @@ -744,10 +823,11 @@ g_resolver_records_from_res_query (const gchar *rrname, { /* Not possible to recover parsing as the length of the rest of the * record is unknown or is too short. */ - g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, - _("Error resolving “%s”"), rrname); - g_list_free_full (records, (GDestroyNotify) g_variant_unref); - return NULL; + g_set_error (&parsing_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the first placeholder is a domain name, the + * second is an error message */ + _("Error resolving “%s”: %s"), rrname, _("Malformed DNS packet")); + break; } p += expand_result; @@ -765,19 +845,19 @@ g_resolver_records_from_res_query (const gchar *rrname, switch (rrtype) { case T_SRV: - record = parse_res_srv (answer, end, &p); + record = parse_res_srv (answer, end, &p, &parsing_error); break; case T_MX: - record = parse_res_mx (answer, end, &p); + record = parse_res_mx (answer, end, &p, &parsing_error); break; case T_SOA: - record = parse_res_soa (answer, end, &p); + record = parse_res_soa (answer, end, &p, &parsing_error); break; case T_NS: - record = parse_res_ns (answer, end, &p); + record = parse_res_ns (answer, end, &p, &parsing_error); break; case T_TXT: - record = parse_res_txt (answer, p + rdlength, &p); + record = parse_res_txt (answer, p + rdlength, &p, &parsing_error); break; default: g_debug ("Unrecognised DNS record type %u", rrtype); @@ -787,9 +867,18 @@ g_resolver_records_from_res_query (const gchar *rrname, if (record != NULL) records = g_list_prepend (records, record); + + if (parsing_error != NULL) + break; } - if (records == NULL) + if (parsing_error != NULL) + { + g_propagate_prefixed_error (error, parsing_error, _("Failed to parse DNS response for “%s”: "), rrname); + g_list_free_full (records, (GDestroyNotify)g_variant_unref); + return NULL; + } + else if (records == NULL) { g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND, _("No DNS record of the requested type for “%s”"), rrname); From 08dee06b59cc0ca909291c40b7765398fd4e10bc Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 18 Mar 2022 16:19:44 +0000 Subject: [PATCH 08/12] gthreadedresolver: Limit length of each record to its stated rdlength Rather than limiting them to the full length of the answer, which may include subsequent records. Signed-off-by: Philip Withnall --- gio/gthreadedresolver.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/gio/gthreadedresolver.c b/gio/gthreadedresolver.c index 63a852d59..143f4570b 100644 --- a/gio/gthreadedresolver.c +++ b/gio/gthreadedresolver.c @@ -836,6 +836,15 @@ g_resolver_records_from_res_query (const gchar *rrname, p += 4; /* ignore the ttl (type=long) value */ GETSHORT (rdlength, p); + if (end - p < rdlength) + { + g_set_error (&parsing_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the first placeholder is a domain name, the + * second is an error message */ + _("Error resolving “%s”: %s"), rrname, _("Malformed DNS packet")); + break; + } + if (type != rrtype || qclass != C_IN) { p += rdlength; @@ -845,16 +854,16 @@ g_resolver_records_from_res_query (const gchar *rrname, switch (rrtype) { case T_SRV: - record = parse_res_srv (answer, end, &p, &parsing_error); + record = parse_res_srv (answer, p + rdlength, &p, &parsing_error); break; case T_MX: - record = parse_res_mx (answer, end, &p, &parsing_error); + record = parse_res_mx (answer, p + rdlength, &p, &parsing_error); break; case T_SOA: - record = parse_res_soa (answer, end, &p, &parsing_error); + record = parse_res_soa (answer, p + rdlength, &p, &parsing_error); break; case T_NS: - record = parse_res_ns (answer, end, &p, &parsing_error); + record = parse_res_ns (answer, p + rdlength, &p, &parsing_error); break; case T_TXT: record = parse_res_txt (answer, p + rdlength, &p, &parsing_error); From 0d42af06e07c8f52811a082714a3a50d3030fb6f Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 21 Mar 2022 14:51:08 +0000 Subject: [PATCH 09/12] gthreadedresolver: Treat query and answer counts as unsigned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They can’t be negative. Signed-off-by: Philip Withnall --- gio/gthreadedresolver.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gio/gthreadedresolver.c b/gio/gthreadedresolver.c index 143f4570b..aeeb40e9b 100644 --- a/gio/gthreadedresolver.c +++ b/gio/gthreadedresolver.c @@ -738,7 +738,7 @@ g_resolver_records_from_res_query (const gchar *rrname, gint herr, GError **error) { - gint count; + uint16_t count; gchar namebuf[1024]; const guint8 *end, *p; guint16 type, qclass, rdlength; From 33204fe12700e8ab57573917f679fe07d855b154 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 21 Mar 2022 17:47:49 +0000 Subject: [PATCH 10/12] tests: Add tests for parsing specific DNS record types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Success and failure tests. This massively increases test coverage for parsing DNS records, although it doesn’t get it to 100%. It should now be useful enough to do more fuzzing on, without immediately getting trivial failures from the fuzzer. Signed-off-by: Philip Withnall --- gio/tests/resolver-parsing.c | 684 +++++++++++++++++++++++++++++++++++ 1 file changed, 684 insertions(+) diff --git a/gio/tests/resolver-parsing.c b/gio/tests/resolver-parsing.c index 909917be8..d9cf05244 100644 --- a/gio/tests/resolver-parsing.c +++ b/gio/tests/resolver-parsing.c @@ -80,6 +80,31 @@ dns_builder_add_domain (GByteArray *builder, g_byte_array_append (builder, buffer, ret); } +/* Append an invalid domain name to the DNS response. This is implemented by + * appending a single label followed by a pointer back to that label. This is + * invalid regardless of any other context in the response as its expansion is + * infinite. + * + * See https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.4 + * + * In order to create a pointer to the label, the label’s final offset in the + * DNS response must be known. The current length of @builder, plus @offset, is + * used for this. Hence, @offset is the additional offset (in bytes) to add, and + * typically corresponds to the length of the parent #GByteArray that @builder + * will eventually be added to. Potentially plus 2 bytes for the rdlength, as + * per dns_builder_add_answer_data(). */ +static void +dns_builder_add_invalid_domain (GByteArray *builder, + gsize offset) +{ + offset += builder->len; + g_assert ((offset & 0xc0) == 0); + + dns_builder_add_uint8 (builder, 1); + dns_builder_add_uint8 (builder, 'f'); + dns_builder_add_uint8 (builder, 0xc0 | offset); +} + static void dns_builder_add_answer_data (GByteArray *builder, GByteArray *answer) @@ -104,6 +129,55 @@ dns_header (void) return g_steal_pointer (&answer); } + +static void +assert_query_fails (const gchar *rrname, + GResolverRecordType record_type, + GByteArray *answer) +{ + GList *records = NULL; + GError *local_error = NULL; + + records = g_resolver_records_from_res_query (rrname, + g_resolver_record_type_to_rrtype (record_type), + answer->data, + answer->len, + 0, + &local_error); + + g_assert_error (local_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL); + g_assert_null (records); + g_clear_error (&local_error); +} + +static void +assert_query_succeeds (const gchar *rrname, + GResolverRecordType record_type, + GByteArray *answer, + const gchar *expected_answer_variant_str) +{ + GList *records = NULL; + GVariant *answer_variant, *expected_answer_variant = NULL; + GError *local_error = NULL; + + records = g_resolver_records_from_res_query (rrname, + g_resolver_record_type_to_rrtype (record_type), + answer->data, + answer->len, + 0, + &local_error); + + g_assert_no_error (local_error); + g_assert_nonnull (records); + + /* Test the results. */ + answer_variant = records->data; + expected_answer_variant = g_variant_new_parsed (expected_answer_variant_str); + g_assert_cmpvariant (answer_variant, expected_answer_variant); + + g_variant_unref (expected_answer_variant); + g_list_free_full (records, (GDestroyNotify) g_variant_unref); +} #endif /* HAVE_DN_COMP */ static void @@ -182,6 +256,598 @@ test_unknown_record_type (void) #endif } +static void +test_mx_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *mx_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_MX)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* MX rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 */ + mx_rdata = g_byte_array_new (); + dns_builder_add_uint16 (mx_rdata, 0); /* preference */ + dns_builder_add_domain (mx_rdata, "mail.example.org"); + dns_builder_add_answer_data (answer, mx_rdata); + g_byte_array_unref (mx_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_MX, answer, + "(@q 0, 'mail.example.org')"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_mx_invalid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *mx_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_MX)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* MX rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 + * + * Use an invalid domain to trigger parsing failure. */ + mx_rdata = g_byte_array_new (); + dns_builder_add_uint16 (mx_rdata, 0); /* preference */ + dns_builder_add_invalid_domain (mx_rdata, answer->len + 2); + dns_builder_add_answer_data (answer, mx_rdata); + g_byte_array_unref (mx_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_MX, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_mx_invalid_too_short (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *mx_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_MX)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* MX rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 + * + * Miss out the domain field to trigger failure */ + mx_rdata = g_byte_array_new (); + dns_builder_add_uint16 (mx_rdata, 0); /* preference */ + /* missing domain field */ + dns_builder_add_answer_data (answer, mx_rdata); + g_byte_array_unref (mx_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_MX, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_mx_invalid_too_short2 (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *mx_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_MX)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* MX rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 + * + * Miss out all fields to trigger failure */ + mx_rdata = g_byte_array_new (); + /* missing preference and domain fields */ + dns_builder_add_answer_data (answer, mx_rdata); + g_byte_array_unref (mx_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_MX, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_ns_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *ns_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_NS)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* NS rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.11 */ + ns_rdata = g_byte_array_new (); + dns_builder_add_domain (ns_rdata, "ns.example.org"); + dns_builder_add_answer_data (answer, ns_rdata); + g_byte_array_unref (ns_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_NS, answer, + "('ns.example.org',)"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_ns_invalid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *ns_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_NS)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* NS rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.11 + * + * Use an invalid domain to trigger parsing failure. */ + ns_rdata = g_byte_array_new (); + dns_builder_add_invalid_domain (ns_rdata, answer->len + 2); + dns_builder_add_answer_data (answer, ns_rdata); + g_byte_array_unref (ns_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_NS, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_soa_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *soa_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SOA)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SOA rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 */ + soa_rdata = g_byte_array_new (); + dns_builder_add_domain (soa_rdata, "mname.example.org"); + dns_builder_add_domain (soa_rdata, "rname.example.org"); + dns_builder_add_uint32 (soa_rdata, 0); /* serial */ + dns_builder_add_uint32 (soa_rdata, 0); /* refresh */ + dns_builder_add_uint32 (soa_rdata, 0); /* retry */ + dns_builder_add_uint32 (soa_rdata, 0); /* expire */ + dns_builder_add_uint32 (soa_rdata, 0); /* minimum */ + dns_builder_add_answer_data (answer, soa_rdata); + g_byte_array_unref (soa_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_SOA, answer, + "('mname.example.org', 'rname.example.org', @u 0, @u 0, @u 0, @u 0, @u 0)"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_soa_invalid_mname (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *soa_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SOA)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SOA rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 + * + * Use an invalid domain to trigger parsing failure. */ + soa_rdata = g_byte_array_new (); + dns_builder_add_invalid_domain (soa_rdata, answer->len + 2); /* mname */ + dns_builder_add_domain (soa_rdata, "rname.example.org"); + dns_builder_add_uint32 (soa_rdata, 0); /* serial */ + dns_builder_add_uint32 (soa_rdata, 0); /* refresh */ + dns_builder_add_uint32 (soa_rdata, 0); /* retry */ + dns_builder_add_uint32 (soa_rdata, 0); /* expire */ + dns_builder_add_uint32 (soa_rdata, 0); /* minimum */ + dns_builder_add_answer_data (answer, soa_rdata); + g_byte_array_unref (soa_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SOA, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_soa_invalid_rname (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *soa_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SOA)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SOA rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 + * + * Use an invalid domain to trigger parsing failure. */ + soa_rdata = g_byte_array_new (); + dns_builder_add_domain (soa_rdata, "mname.example.org"); + dns_builder_add_invalid_domain (soa_rdata, answer->len + 2); /* rname */ + dns_builder_add_uint32 (soa_rdata, 0); /* serial */ + dns_builder_add_uint32 (soa_rdata, 0); /* refresh */ + dns_builder_add_uint32 (soa_rdata, 0); /* retry */ + dns_builder_add_uint32 (soa_rdata, 0); /* expire */ + dns_builder_add_uint32 (soa_rdata, 0); /* minimum */ + dns_builder_add_answer_data (answer, soa_rdata); + g_byte_array_unref (soa_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SOA, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_soa_invalid_too_short (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *soa_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SOA)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SOA rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 + * + * Miss out one of the fields to trigger a failure. */ + soa_rdata = g_byte_array_new (); + dns_builder_add_domain (soa_rdata, "mname.example.org"); + dns_builder_add_domain (soa_rdata, "rname.example.org"); + dns_builder_add_uint32 (soa_rdata, 0); /* serial */ + dns_builder_add_uint32 (soa_rdata, 0); /* refresh */ + dns_builder_add_uint32 (soa_rdata, 0); /* retry */ + dns_builder_add_uint32 (soa_rdata, 0); /* expire */ + /* missing minimum field */ + dns_builder_add_answer_data (answer, soa_rdata); + g_byte_array_unref (soa_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SOA, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_txt_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *txt_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_TXT)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* TXT rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 */ + txt_rdata = g_byte_array_new (); + dns_builder_add_length_prefixed_string (txt_rdata, "some test content"); + dns_builder_add_answer_data (answer, txt_rdata); + g_byte_array_unref (txt_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_TXT, answer, + "(['some test content'],)"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_txt_valid_multiple_strings (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *txt_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_TXT)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* TXT rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 */ + txt_rdata = g_byte_array_new (); + dns_builder_add_length_prefixed_string (txt_rdata, "some test content"); + dns_builder_add_length_prefixed_string (txt_rdata, "more test content"); + dns_builder_add_answer_data (answer, txt_rdata); + g_byte_array_unref (txt_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_TXT, answer, + "(['some test content', 'more test content'],)"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_txt_invalid_empty (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *txt_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_TXT)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* TXT rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 + * + * Provide zero character strings (i.e. an empty rdata section) to trigger + * failure. */ + txt_rdata = g_byte_array_new (); + dns_builder_add_answer_data (answer, txt_rdata); + g_byte_array_unref (txt_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_TXT, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_txt_invalid_overflow (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *txt_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_TXT)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* TXT rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 + * + * Use a character string whose length exceeds the remaining length in the + * answer record, to trigger failure. */ + txt_rdata = g_byte_array_new (); + dns_builder_add_uint8 (txt_rdata, 10); /* length, but no content */ + dns_builder_add_answer_data (answer, txt_rdata); + g_byte_array_unref (txt_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_TXT, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_srv_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *srv_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SRV)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SRV rdata, https://datatracker.ietf.org/doc/html/rfc2782 */ + srv_rdata = g_byte_array_new (); + dns_builder_add_uint16 (srv_rdata, 0); /* priority */ + dns_builder_add_uint16 (srv_rdata, 0); /* weight */ + dns_builder_add_uint16 (srv_rdata, 0); /* port */ + dns_builder_add_domain (srv_rdata, "target.example.org"); + dns_builder_add_answer_data (answer, srv_rdata); + g_byte_array_unref (srv_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_SRV, answer, + "(@q 0, @q 0, @q 0, 'target.example.org')"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_srv_invalid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *srv_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SRV)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SRV rdata, https://datatracker.ietf.org/doc/html/rfc2782 + * + * Use an invalid domain to trigger parsing failure. */ + srv_rdata = g_byte_array_new (); + dns_builder_add_uint16 (srv_rdata, 0); /* priority */ + dns_builder_add_uint16 (srv_rdata, 0); /* weight */ + dns_builder_add_uint16 (srv_rdata, 0); /* port */ + dns_builder_add_invalid_domain (srv_rdata, answer->len + 2); + dns_builder_add_answer_data (answer, srv_rdata); + g_byte_array_unref (srv_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SRV, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_srv_invalid_too_short (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *srv_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SRV)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SRV rdata, https://datatracker.ietf.org/doc/html/rfc2782 + * + * Miss out the target field to trigger failure */ + srv_rdata = g_byte_array_new (); + dns_builder_add_uint16 (srv_rdata, 0); /* priority */ + dns_builder_add_uint16 (srv_rdata, 0); /* weight */ + dns_builder_add_uint16 (srv_rdata, 0); /* port */ + /* missing target field */ + dns_builder_add_answer_data (answer, srv_rdata); + g_byte_array_unref (srv_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SRV, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_srv_invalid_too_short2 (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *srv_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SRV)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SRV rdata, https://datatracker.ietf.org/doc/html/rfc2782 + * + * Miss out the target and port fields to trigger failure */ + srv_rdata = g_byte_array_new (); + dns_builder_add_uint16 (srv_rdata, 0); /* priority */ + dns_builder_add_uint16 (srv_rdata, 0); /* weight */ + /* missing port and target fields */ + dns_builder_add_answer_data (answer, srv_rdata); + g_byte_array_unref (srv_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SRV, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + int main (int argc, char *argv[]) @@ -190,6 +856,24 @@ main (int argc, g_test_add_func ("/gresolver/invalid-header", test_invalid_header); g_test_add_func ("/gresolver/unknown-record-type", test_unknown_record_type); + g_test_add_func ("/gresolver/mx/valid", test_mx_valid); + g_test_add_func ("/gresolver/mx/invalid", test_mx_invalid); + g_test_add_func ("/gresolver/mx/invalid/too-short", test_mx_invalid_too_short); + g_test_add_func ("/gresolver/mx/invalid/too-short2", test_mx_invalid_too_short2); + g_test_add_func ("/gresolver/ns/valid", test_ns_valid); + g_test_add_func ("/gresolver/ns/invalid", test_ns_invalid); + g_test_add_func ("/gresolver/soa/valid", test_soa_valid); + g_test_add_func ("/gresolver/soa/invalid/mname", test_soa_invalid_mname); + g_test_add_func ("/gresolver/soa/invalid/rname", test_soa_invalid_rname); + g_test_add_func ("/gresolver/soa/invalid/too-short", test_soa_invalid_too_short); + g_test_add_func ("/gresolver/srv/valid", test_srv_valid); + g_test_add_func ("/gresolver/srv/invalid", test_srv_invalid); + g_test_add_func ("/gresolver/srv/invalid/too-short", test_srv_invalid_too_short); + g_test_add_func ("/gresolver/srv/invalid/too-short2", test_srv_invalid_too_short2); + g_test_add_func ("/gresolver/txt/valid", test_txt_valid); + g_test_add_func ("/gresolver/txt/valid/multiple-strings", test_txt_valid_multiple_strings); + g_test_add_func ("/gresolver/txt/invalid/empty", test_txt_invalid_empty); + g_test_add_func ("/gresolver/txt/invalid/overflow", test_txt_invalid_overflow); return g_test_run (); } From e8e8aebcbeac5791db05d13e73fb81282adc3810 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 21 Mar 2022 17:53:25 +0000 Subject: [PATCH 11/12] resolver: Add SRV support to manual resolver test This allows for tests like: ``` resolver -t SRV _http._tcp.mxtoolbox.com ``` Signed-off-by: Philip Withnall --- gio/tests/resolver.c | 54 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/gio/tests/resolver.c b/gio/tests/resolver.c index 6e0c4d73b..d62a4fd18 100644 --- a/gio/tests/resolver.c +++ b/gio/tests/resolver.c @@ -44,12 +44,12 @@ static G_NORETURN void usage (void) { fprintf (stderr, "Usage: resolver [-s] [hostname | IP | service/protocol/domain ] ...\n"); - fprintf (stderr, "Usage: resolver [-s] [-t MX|TXT|NS|SOA] rrname ...\n"); + fprintf (stderr, "Usage: resolver [-s] [-t MX|TXT|NS|SOA|SRV] rrname ...\n"); fprintf (stderr, " resolver [-s] -c NUMBER [hostname | IP | service/protocol/domain ]\n"); fprintf (stderr, " Use -s to do synchronous lookups.\n"); fprintf (stderr, " Use -c NUMBER (and only a single resolvable argument) to test GSocketConnectable.\n"); fprintf (stderr, " The given NUMBER determines how many times the connectable will be enumerated.\n"); - fprintf (stderr, " Use -t with MX, TXT, NS or SOA to look up DNS records of those types.\n"); + fprintf (stderr, " Use -t with MX, TXT, NS, SOA or SRV to look up DNS records of those types.\n"); exit (1); } @@ -232,6 +232,46 @@ print_resolved_txt (const char *rrname, G_UNLOCK (response); } +static void +print_resolved_srv (const char *rrname, + GList *records, + GError *error) +{ + G_LOCK (response); + printf ("Domain: %s\n", rrname); + if (error) + { + printf ("Error: %s\n", error->message); + g_error_free (error); + } + else if (!records) + { + printf ("no SRV records\n"); + } + else + { + GList *t; + + for (t = records; t != NULL; t = t->next) + { + guint16 priority, weight, port; + const gchar *target; + + g_variant_get (t->data, "(qqq&s)", &priority, &weight, &port, &target); + + printf ("%s (priority %u, weight %u, port %u)\n", + target, (guint) priority, (guint) weight, (guint) port); + g_variant_unref (t->data); + } + + g_list_free (records); + } + printf ("\n"); + + done_lookup (); + G_UNLOCK (response); +} + static void print_resolved_soa (const char *rrname, GList *records, @@ -331,6 +371,9 @@ lookup_one_sync (const char *arg) case G_RESOLVER_RECORD_TXT: print_resolved_txt (arg, records, error); break; + case G_RESOLVER_RECORD_SRV: + print_resolved_srv (arg, records, error); + break; default: g_warn_if_reached (); break; @@ -449,6 +492,9 @@ lookup_records_callback (GObject *source, case G_RESOLVER_RECORD_TXT: print_resolved_txt (arg, records, error); break; + case G_RESOLVER_RECORD_SRV: + print_resolved_srv (arg, records, error); + break; default: g_warn_if_reached (); break; @@ -659,9 +705,11 @@ record_type_arg (const gchar *option_name, record_type = G_RESOLVER_RECORD_SOA; } else if (g_ascii_strcasecmp (value, "NS") == 0) { record_type = G_RESOLVER_RECORD_NS; + } else if (g_ascii_strcasecmp (value, "SRV") == 0) { + record_type = G_RESOLVER_RECORD_SRV; } else { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, - "Specify MX, TXT, NS or SOA for the special record lookup types"); + "Specify MX, TXT, NS, SOA or SRV for the special record lookup types"); return FALSE; } From f9ef3bec68981db2b476d5c6cd455ec00606bf94 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 22 Mar 2022 12:41:08 +0000 Subject: [PATCH 12/12] gthreadedresolver: Only declare private test APIs on Unix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They’re only defined on Unix anyway. `GThreadedResolver` has an entirely different code path for handling DNS replies on Windows. Signed-off-by: Philip Withnall --- gio/gthreadedresolver.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gio/gthreadedresolver.h b/gio/gthreadedresolver.h index c4d6dc28d..8d2ca19bf 100644 --- a/gio/gthreadedresolver.h +++ b/gio/gthreadedresolver.h @@ -43,6 +43,7 @@ GLIB_AVAILABLE_IN_ALL GType g_threaded_resolver_get_type (void) G_GNUC_CONST; /* Used for a private test API */ +#ifdef G_OS_UNIX GLIB_AVAILABLE_IN_ALL GList *g_resolver_records_from_res_query (const gchar *rrname, gint rrtype, @@ -52,6 +53,7 @@ GList *g_resolver_records_from_res_query (const gchar *rrname, GError **error); GLIB_AVAILABLE_IN_ALL gint g_resolver_record_type_to_rrtype (GResolverRecordType type); +#endif G_END_DECLS