diff --git a/meson.build b/meson.build index df6e5ae1..813a8592 100644 --- a/meson.build +++ b/meson.build @@ -228,5 +228,6 @@ summary({'Custom Python': get_option('python_name'), 'Python 3 Support': get_option('with_py3'), 'Skip Introspection': get_option('skip_introspection'), 'Accept overflowed buildorder': get_option('accept_overflowed_buildorder'), + 'Recursion limit': get_option('recursion_limit'), 'Test Installed Library': get_option('test_installed_lib'), }, section: 'Build Configuration') diff --git a/meson_options.txt b/meson_options.txt index c347afa0..1c668231 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -23,6 +23,9 @@ option('libmagic', type : 'feature', value : 'auto', option('python_name', type : 'string', description : 'The name of the Python 3 interpreter to use for generating Python bindings and running tests. If left blank, it defaults to the version of Python 3 being used to run meson.') +option('recursion_limit', type : 'integer', min : 16, value : 64, + description : 'Maximal depth of accepted recusive structures, like mappings in YAML documents.') + option('rpmio', type : 'feature', value : 'enabled', description : 'Use the rpmio library to automatically decompress gzip, bzip2 and xz YAML streams.') diff --git a/modulemd/include/private/modulemd-yaml.h b/modulemd/include/private/modulemd-yaml.h index 105063b6..10cd31b5 100644 --- a/modulemd/include/private/modulemd-yaml.h +++ b/modulemd/include/private/modulemd-yaml.h @@ -913,6 +913,7 @@ mmd_variant_from_scalar (const gchar *scalar); /** * mmd_variant_from_mapping: * @parser: (inout): A YAML parser positioned just after a `MAPPING_START` + * @depth: Current YAML depth in the XMD document * @error: (out): A #GError that will return the reason for failing to parse. * * Returns: (transfer full): A new, floating #GVariant representing a hash @@ -921,12 +922,13 @@ mmd_variant_from_scalar (const gchar *scalar); * Since: 2.0 */ GVariant * -mmd_variant_from_mapping (yaml_parser_t *parser, GError **error); +mmd_variant_from_mapping (yaml_parser_t *parser, int depth, GError **error); /** * mmd_variant_from_sequence: * @parser: (inout): A YAML parser positioned just after a `SEQUENCE_START` + * @depth: Current YAML depth in the XMD document * @error: (out): A #GError that will return the reason for failing to parse. * * Returns: (transfer full): A new, floating #GVariant representing a list of @@ -935,7 +937,7 @@ mmd_variant_from_mapping (yaml_parser_t *parser, GError **error); * Since: 2.0 */ GVariant * -mmd_variant_from_sequence (yaml_parser_t *parser, GError **error); +mmd_variant_from_sequence (yaml_parser_t *parser, int depth, GError **error); /** diff --git a/modulemd/meson.build b/modulemd/meson.build index e41bcc52..0b87913d 100644 --- a/modulemd/meson.build +++ b/modulemd/meson.build @@ -16,6 +16,7 @@ skip_introspection = get_option('skip_introspection') verbose_tests = get_option('verbose_tests') accept_overflowed_buildorder = get_option('accept_overflowed_buildorder') clang_simple_version_script = find_program ('clang_simple_version.sh') +recursion_limit = get_option('recursion_limit') # -- Library -- # @@ -147,6 +148,7 @@ cdata.set('HAVE_EXTEND_AND_STEAL', has_extend_and_steal) cdata.set('HAVE_G_SPAWN_CHECK_WAIT_STATUS', has_g_spawn_check_wait_status) cdata.set('HAVE_G_TEST_SUBPROCESS_DEFAULT', has_g_test_subprocess_default) cdata.set('HAVE_OVERFLOWED_BUILDORDER', accept_overflowed_buildorder) +cdata.set('LIBMODULEMD_RECURSION_LIMIT', recursion_limit) configure_file( output : 'config.h', configuration : cdata @@ -355,6 +357,7 @@ c_tests = { 'defaults' : [ 'tests/test-modulemd-defaults.c' ], 'defaultsv1' : [ 'tests/test-modulemd-defaults-v1.c' ], 'dependencies' : [ 'tests/test-modulemd-dependencies.c' ], +'limits' : [ 'tests/test-modulemd-limits.c' ], 'module' : [ 'tests/test-modulemd-module.c' ], 'module_index' : [ 'tests/test-modulemd-moduleindex.c' ], 'module_index_merger' : [ 'tests/test-modulemd-merger.c' ], diff --git a/modulemd/modulemd-yaml-util.c b/modulemd/modulemd-yaml-util.c index be5fb566..47d0bda6 100644 --- a/modulemd/modulemd-yaml-util.c +++ b/modulemd/modulemd-yaml-util.c @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -1085,7 +1086,14 @@ modulemd_yaml_parse_document_type_internal ( { return FALSE; } - depth++; + if (depth == INT_MAX || ++depth > LIBMODULEMD_RECURSION_LIMIT) + { + MMD_YAML_ERROR_EVENT_EXIT_BOOL ( + error, + event, + "YAML document depth limit %d exceeded.", + LIBMODULEMD_RECURSION_LIMIT); + } break; case YAML_SCALAR_EVENT: @@ -1488,7 +1496,7 @@ mmd_variant_from_scalar (const gchar *scalar) GVariant * -mmd_variant_from_mapping (yaml_parser_t *parser, GError **error) +mmd_variant_from_mapping (yaml_parser_t *parser, int depth, GError **error) { MODULEMD_INIT_TRACE (); gboolean done = FALSE; @@ -1532,7 +1540,16 @@ mmd_variant_from_mapping (yaml_parser_t *parser, GError **error) break; case YAML_MAPPING_START_EVENT: - value = mmd_variant_from_mapping (parser, &nested_error); + if (depth == INT_MAX || depth + 1 > LIBMODULEMD_RECURSION_LIMIT) + { + MMD_YAML_ERROR_EVENT_EXIT ( + error, + event, + "XMD YAML document depth limit %d exceeded.", + LIBMODULEMD_RECURSION_LIMIT); + } + value = + mmd_variant_from_mapping (parser, depth + 1, &nested_error); if (!value) { g_propagate_error (error, g_steal_pointer (&nested_error)); @@ -1541,7 +1558,16 @@ mmd_variant_from_mapping (yaml_parser_t *parser, GError **error) break; case YAML_SEQUENCE_START_EVENT: - value = mmd_variant_from_sequence (parser, &nested_error); + if (depth == INT_MAX || depth + 1 > LIBMODULEMD_RECURSION_LIMIT) + { + MMD_YAML_ERROR_EVENT_EXIT ( + error, + event, + "XMD YAML document depth limit %d exceeded.", + LIBMODULEMD_RECURSION_LIMIT); + } + value = + mmd_variant_from_sequence (parser, depth + 1, &nested_error); if (!value) { g_propagate_error (error, g_steal_pointer (&nested_error)); @@ -1582,7 +1608,7 @@ mmd_variant_from_mapping (yaml_parser_t *parser, GError **error) GVariant * -mmd_variant_from_sequence (yaml_parser_t *parser, GError **error) +mmd_variant_from_sequence (yaml_parser_t *parser, int depth, GError **error) { MODULEMD_INIT_TRACE (); gboolean done = FALSE; @@ -1617,7 +1643,15 @@ mmd_variant_from_sequence (yaml_parser_t *parser, GError **error) break; case YAML_MAPPING_START_EVENT: - value = mmd_variant_from_mapping (parser, &nested_error); + if (depth == INT_MAX || depth + 1 > LIBMODULEMD_RECURSION_LIMIT) + { + MMD_YAML_ERROR_EVENT_EXIT ( + error, + event, + "XMD YAML document depth limit %d exceeded.", + LIBMODULEMD_RECURSION_LIMIT); + } + value = mmd_variant_from_mapping (parser, depth + 1, &nested_error); if (!value) { g_propagate_error (error, g_steal_pointer (&nested_error)); @@ -1626,7 +1660,15 @@ mmd_variant_from_sequence (yaml_parser_t *parser, GError **error) break; case YAML_SEQUENCE_START_EVENT: - value = mmd_variant_from_sequence (parser, &nested_error); + if (depth == INT_MAX || depth + 1 > LIBMODULEMD_RECURSION_LIMIT) + { + MMD_YAML_ERROR_EVENT_EXIT ( + error, + event, + "XMD YAML document depth limit %d exceeded.", + LIBMODULEMD_RECURSION_LIMIT); + } + value = mmd_variant_from_sequence (parser, depth + 1, &nested_error); if (!value) { g_propagate_error (error, g_steal_pointer (&nested_error)); @@ -1677,6 +1719,7 @@ mmd_parse_xmd (yaml_parser_t *parser, GError **error) MMD_INIT_YAML_EVENT (event); GVariant *variant = NULL; g_autoptr (GError) nested_error = NULL; + int depth = 0; g_return_val_if_fail (error == NULL || *error == NULL, FALSE); @@ -1694,7 +1737,20 @@ mmd_parse_xmd (yaml_parser_t *parser, GError **error) break; case YAML_MAPPING_START_EVENT: - variant = mmd_variant_from_mapping (parser, &nested_error); + if (depth == INT_MAX || depth + 1 > LIBMODULEMD_RECURSION_LIMIT) + { + MMD_YAML_ERROR_EVENT_EXIT ( + error, + event, + "XMD YAML document depth limit %d exceeded.", + LIBMODULEMD_RECURSION_LIMIT); + } + variant = mmd_variant_from_mapping (parser, depth + 1, &nested_error); + if (!variant) + { + g_propagate_error (error, g_steal_pointer (&nested_error)); + return NULL; + } break; default: diff --git a/modulemd/tests/test-modulemd-limits.c b/modulemd/tests/test-modulemd-limits.c new file mode 100644 index 00000000..56c0c671 --- /dev/null +++ b/modulemd/tests/test-modulemd-limits.c @@ -0,0 +1,319 @@ +/* + * This file is part of libmodulemd + * Copyright (C) 2018-2020 Red Hat, Inc. + * + * Fedora-License-Identifier: MIT + * SPDX-2.0-License-Identifier: MIT + * SPDX-3.0-License-Identifier: MIT + * + * This program is free software. + * For more information on the license, see COPYING. + * For more information on free software, see . + */ + +#include +#include +#include + +#include "config.h" +#include "modulemd-subdocument-info.h" +#include "private/modulemd-subdocument-info-private.h" +#include "private/modulemd-yaml.h" + +/* Return a modulemd-stream-v2 document with an xmd tree of @depth depth. + * I.e. the total depth of the YAML document will be @depth + 2. */ +static GString * +build_modulemd_stream_v2_document (int depth) +{ + g_autoptr (GString) string = g_string_new (NULL); + g_string_append (string, + "document: modulemd\n" + "version: 2\n" + "data:\n" + " name: demo\n" + " stream: s\n" + " version: 1\n" + " context: c0\n" + " arch: x86_64\n" + " summary: s\n" + " description: d\n" + " license: { module: [MIT] }\n" + " xmd: {\n"); + g_string_append (string, " "); + for (int i = 1; i < depth; i++) + g_string_append (string, "a: {"); + for (int i = 1; i < depth; i++) + g_string_append (string, "}"); + g_string_append (string, "\n }\n"); + + return g_steal_pointer (&string); +} + + +static void +module_limits_test_parse_document_type_exact (void) +{ + g_autoptr (GString) yaml_string = NULL; + MMD_INIT_YAML_EVENT (event); + MMD_INIT_YAML_PARSER (parser); + int yaml_ret; + g_autoptr (ModulemdSubdocumentInfo) subdoc = NULL; + + /* Build a document with a depth matching, i.e. fitting, the recursion limit. */ + yaml_string = + build_modulemd_stream_v2_document (LIBMODULEMD_RECURSION_LIMIT - 2); + + /* Advance the parser to the first document element */ + yaml_parser_set_input_string ( + &parser, (const unsigned char *)yaml_string->str, yaml_string->len); + /* The first event must be the stream start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_STREAM_START_EVENT); + yaml_event_delete (&event); + /* The second event must be the document start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_DOCUMENT_START_EVENT); + yaml_event_delete (&event); + + /* Parse it and exhibit the recursion limit */ + subdoc = modulemd_yaml_parse_document_type (&parser); + g_assert_nonnull (subdoc); + g_assert_null (modulemd_subdocument_info_get_gerror (subdoc)); + g_assert_cmpint (modulemd_subdocument_info_get_doctype (subdoc), + ==, + MODULEMD_YAML_DOC_MODULESTREAM); + g_assert_cmpint (modulemd_subdocument_info_get_mdversion (subdoc), ==, 2); + g_assert_nonnull (modulemd_subdocument_info_get_yaml (subdoc)); +} + + +static void +module_limits_test_parse_document_type_oversized (void) +{ + g_autoptr (GString) yaml_string = NULL; + MMD_INIT_YAML_EVENT (event); + MMD_INIT_YAML_PARSER (parser); + int yaml_ret; + g_autoptr (ModulemdSubdocumentInfo) subdoc = NULL; + const GError *error = NULL; + + /* Build a document with a depth exceeding the recursion limit. */ + yaml_string = + build_modulemd_stream_v2_document (LIBMODULEMD_RECURSION_LIMIT - 2 + 1); + + /* Advance the parser to the first document element */ + yaml_parser_set_input_string ( + &parser, (const unsigned char *)yaml_string->str, yaml_string->len); + /* The first event must be the stream start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_STREAM_START_EVENT); + yaml_event_delete (&event); + /* The second event must be the document start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_DOCUMENT_START_EVENT); + yaml_event_delete (&event); + + /* Parse it and exhibit the recursion limit */ + subdoc = modulemd_yaml_parse_document_type (&parser); + g_assert_nonnull (subdoc); + error = modulemd_subdocument_info_get_gerror (subdoc); + g_assert_error (error, MODULEMD_YAML_ERROR, MMD_YAML_ERROR_PARSE); +} + + +/* Return an XMD YAML document with a mapping of @depth depth. */ +static GString * +build_xmd_mapping_document (int depth) +{ + g_autoptr (GString) string = g_string_new (NULL); + for (int i = 1; i < depth; i++) + g_string_append (string, "a: {"); + for (int i = 1; i < depth; i++) + g_string_append (string, "}"); + g_string_append (string, "\n"); + + return g_steal_pointer (&string); +} + + +static void +module_limits_test_mmd_parse_xmd_mapping_exact (void) +{ + g_autoptr (GString) yaml_string = NULL; + MMD_INIT_YAML_EVENT (event); + MMD_INIT_YAML_PARSER (parser); + int yaml_ret; + g_autoptr (GVariant) xmd = NULL; + g_autoptr (GError) error = NULL; + + /* Build a document with a depth matching, i.e. fitting, the recursion limit. */ + yaml_string = build_xmd_mapping_document (LIBMODULEMD_RECURSION_LIMIT); + + /* Advance the parser to the first document element */ + yaml_parser_set_input_string ( + &parser, (const unsigned char *)yaml_string->str, yaml_string->len); + /* The first event must be the stream start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_STREAM_START_EVENT); + yaml_event_delete (&event); + /* The second event must be the document start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_DOCUMENT_START_EVENT); + yaml_event_delete (&event); + + /* Parse it and exhibit the recursion limit */ + xmd = mmd_parse_xmd (&parser, &error); + g_assert_nonnull (xmd); + g_assert_null (error); +} + + +static void +module_limits_test_mmd_parse_xmd_mapping_oversized (void) +{ + g_autoptr (GString) yaml_string = NULL; + MMD_INIT_YAML_EVENT (event); + MMD_INIT_YAML_PARSER (parser); + int yaml_ret; + g_autoptr (GVariant) xmd = NULL; + g_autoptr (GError) error = NULL; + + /* Build a document with a depth exceeding the recursion limit. */ + yaml_string = build_xmd_mapping_document (LIBMODULEMD_RECURSION_LIMIT + 1); + + /* Advance the parser to the first document element */ + yaml_parser_set_input_string ( + &parser, (const unsigned char *)yaml_string->str, yaml_string->len); + /* The first event must be the stream start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_STREAM_START_EVENT); + yaml_event_delete (&event); + /* The second event must be the document start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_DOCUMENT_START_EVENT); + yaml_event_delete (&event); + + /* Parse it and exhibit the recursion limit */ + xmd = mmd_parse_xmd (&parser, &error); + g_assert_null (xmd); + g_assert_error (error, MODULEMD_YAML_ERROR, MMD_YAML_ERROR_PARSE); +} + + +/* Return an XMD YAML document with a recursive sequence of @depth depth. */ +static GString * +build_xmd_sequence_document (int depth) +{ + g_autoptr (GString) string = g_string_new (NULL); + /* mmd_parse_xmd() requires the top-level element to be a mapping. */ + g_string_append (string, "a: "); + for (int i = 1; i < depth; i++) + g_string_append (string, "["); + for (int i = 1; i < depth; i++) + g_string_append (string, "]"); + g_string_append (string, "\n"); + + return g_steal_pointer (&string); +} + + +static void +module_limits_test_mmd_parse_xmd_sequence_exact (void) +{ + g_autoptr (GString) yaml_string = NULL; + MMD_INIT_YAML_EVENT (event); + MMD_INIT_YAML_PARSER (parser); + int yaml_ret; + g_autoptr (GVariant) xmd = NULL; + g_autoptr (GError) error = NULL; + + /* Build a document with a depth matching, i.e. fitting, the recursion limit. */ + yaml_string = build_xmd_sequence_document (LIBMODULEMD_RECURSION_LIMIT); + + /* Advance the parser to the first document element */ + yaml_parser_set_input_string ( + &parser, (const unsigned char *)yaml_string->str, yaml_string->len); + /* The first event must be the stream start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_STREAM_START_EVENT); + yaml_event_delete (&event); + /* The second event must be the document start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_DOCUMENT_START_EVENT); + yaml_event_delete (&event); + + /* Parse it and exhibit the recursion limit */ + xmd = mmd_parse_xmd (&parser, &error); + g_assert_nonnull (xmd); + g_assert_null (error); +} + + +static void +module_limits_test_mmd_parse_xmd_sequence_oversized (void) +{ + g_autoptr (GString) yaml_string = NULL; + MMD_INIT_YAML_EVENT (event); + MMD_INIT_YAML_PARSER (parser); + int yaml_ret; + g_autoptr (GVariant) xmd = NULL; + g_autoptr (GError) error = NULL; + + /* Build a document with a depth matching, i.e. fitting, the recursion limit. */ + yaml_string = build_xmd_sequence_document (LIBMODULEMD_RECURSION_LIMIT + 1); + + /* Advance the parser to the first document element */ + yaml_parser_set_input_string ( + &parser, (const unsigned char *)yaml_string->str, yaml_string->len); + /* The first event must be the stream start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_STREAM_START_EVENT); + yaml_event_delete (&event); + /* The second event must be the document start */ + yaml_ret = yaml_parser_parse (&parser, &event); + g_assert_true (yaml_ret); + g_assert_cmpint (event.type, ==, YAML_DOCUMENT_START_EVENT); + yaml_event_delete (&event); + + /* Parse it and exhibit the recursion limit */ + xmd = mmd_parse_xmd (&parser, &error); + g_assert_null (xmd); + g_assert_error (error, MODULEMD_YAML_ERROR, MMD_YAML_ERROR_PARSE); +} + + +int +main (int argc, char *argv[]) +{ + setlocale (LC_ALL, ""); + g_test_init (&argc, &argv, NULL); + g_test_bug_base ("https://bugzilla.redhat.com/show_bug.cgi?id="); + + g_test_add_func ("/modulemd/v2/limits/parse_document_type/exact", + module_limits_test_parse_document_type_exact); + g_test_add_func ("/modulemd/v2/limits/parse_document_type/oversized", + module_limits_test_parse_document_type_oversized); + + g_test_add_func ("/modulemd/v2/limits/mmd_parse_xmd/mapping/exact", + module_limits_test_mmd_parse_xmd_mapping_exact); + g_test_add_func ("/modulemd/v2/limits/mmd_parse_xmd/mapping/oversized", + module_limits_test_mmd_parse_xmd_mapping_oversized); + + g_test_add_func ("/modulemd/v2/limits/mmd_parse_xmd/sequence/exact", + module_limits_test_mmd_parse_xmd_sequence_exact); + g_test_add_func ("/modulemd/v2/limits/mmd_parse_xmd/sequence/oversized", + module_limits_test_mmd_parse_xmd_sequence_oversized); + + return g_test_run (); +}