From 31f9053c3c46741f4daf2ea2bdea75f40f720d42 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Tue, 2 Sep 2025 22:36:49 +0200 Subject: [PATCH] tests: Cover allocation tracking and limiting with tests CVE: CVE-2025-59375 Upstream-Status: Backport [https://github.com/libexpat/libexpat/commit/31f9053c3c46741f4daf2ea2bdea75f40f720d42] Signed-off-by: Peter Marko --- lib/internal.h | 3 + lib/xmlparse.c | 12 +++ tests/alloc_tests.c | 214 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+) diff --git a/lib/internal.h b/lib/internal.h index eb67cf50..6e087858 100644 --- a/lib/internal.h +++ b/lib/internal.h @@ -173,6 +173,9 @@ extern #endif XML_Bool g_reparseDeferralEnabledDefault; // written ONLY in runtests.c #if defined(XML_TESTING) +void *expat_malloc(XML_Parser parser, size_t size, int sourceLine); +void expat_free(XML_Parser parser, void *ptr, int sourceLine); +void *expat_realloc(XML_Parser parser, void *ptr, size_t size, int sourceLine); extern unsigned int g_bytesScanned; // used for testing only #endif diff --git a/lib/xmlparse.c b/lib/xmlparse.c index d0b6e0cd..6e9c6fb2 100644 --- a/lib/xmlparse.c +++ b/lib/xmlparse.c @@ -843,7 +843,11 @@ expat_heap_increase_tolerable(XML_Parser rootParser, XmlBigCount increase, return tolerable; } +# if defined(XML_TESTING) +void * +# else static void * +# endif expat_malloc(XML_Parser parser, size_t size, int sourceLine) { // Detect integer overflow if (SIZE_MAX - size < sizeof(size_t)) { @@ -893,7 +897,11 @@ expat_malloc(XML_Parser parser, size_t size, int sourceLine) { return (char *)mallocedPtr + sizeof(size_t); } +# if defined(XML_TESTING) +void +# else static void +# endif expat_free(XML_Parser parser, void *ptr, int sourceLine) { assert(parser != NULL); @@ -924,7 +932,11 @@ expat_free(XML_Parser parser, void *ptr, int sourceLine) { parser->m_mem.free_fcn(mallocedPtr); } +# if defined(XML_TESTING) +void * +# else static void * +# endif expat_realloc(XML_Parser parser, void *ptr, size_t size, int sourceLine) { assert(parser != NULL); diff --git a/tests/alloc_tests.c b/tests/alloc_tests.c index 4c3e2af4..275f92d5 100644 --- a/tests/alloc_tests.c +++ b/tests/alloc_tests.c @@ -46,10 +46,16 @@ # undef NDEBUG /* because test suite relies on assert(...) at the moment */ #endif +#include /* NAN, INFINITY */ +#include +#include /* for SIZE_MAX */ #include #include +#include "expat_config.h" + #include "expat.h" +#include "internal.h" #include "common.h" #include "minicheck.h" #include "dummy.h" @@ -2085,6 +2091,203 @@ START_TEST(test_alloc_reset_after_external_entity_parser_create_fail) { } END_TEST +START_TEST(test_alloc_tracker_size_recorded) { + XML_Memory_Handling_Suite memsuite = {malloc, realloc, free}; + + bool values[] = {true, false}; + for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) { + const bool useMemSuite = values[i]; + set_subtest("useMemSuite=%d", (int)useMemSuite); + XML_Parser parser = useMemSuite + ? XML_ParserCreate_MM(NULL, &memsuite, XCS("|")) + : XML_ParserCreate(NULL); + +#if XML_GE == 1 + void *ptr = expat_malloc(parser, 10, -1); + + assert_true(ptr != NULL); + assert_true(*((size_t *)ptr - 1) == 10); + + assert_true(expat_realloc(parser, ptr, SIZE_MAX / 2, -1) == NULL); + + assert_true(*((size_t *)ptr - 1) == 10); // i.e. unchanged + + ptr = expat_realloc(parser, ptr, 20, -1); + + assert_true(ptr != NULL); + assert_true(*((size_t *)ptr - 1) == 20); + + expat_free(parser, ptr, -1); +#endif + + XML_ParserFree(parser); + } +} +END_TEST + +START_TEST(test_alloc_tracker_maximum_amplification) { + if (g_reparseDeferralEnabledDefault == XML_TRUE) { + return; + } + + XML_Parser parser = XML_ParserCreate(NULL); + + // Get .m_accounting.countBytesDirect from 0 to 3 + const char *const chunk = ""; + assert_true(_XML_Parse_SINGLE_BYTES(parser, chunk, (int)strlen(chunk), + /*isFinal=*/XML_FALSE) + == XML_STATUS_OK); + +#if XML_GE == 1 + // Stop activation threshold from interfering + assert_true(XML_SetAllocTrackerActivationThreshold(parser, 0) == XML_TRUE); + + // Exceed maximum amplification: should be rejected. + assert_true(expat_malloc(parser, 1000, -1) == NULL); + + // Increase maximum amplification, and try the same amount once more: should + // work. + assert_true(XML_SetAllocTrackerMaximumAmplification(parser, 3000.0f) + == XML_TRUE); + + void *const ptr = expat_malloc(parser, 1000, -1); + assert_true(ptr != NULL); + expat_free(parser, ptr, -1); +#endif + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_alloc_tracker_threshold) { + XML_Parser parser = XML_ParserCreate(NULL); + +#if XML_GE == 1 + // Exceed maximum amplification *before* (default) threshold: should work. + void *const ptr = expat_malloc(parser, 1000, -1); + assert_true(ptr != NULL); + expat_free(parser, ptr, -1); + + // Exceed maximum amplification *after* threshold: should be rejected. + assert_true(XML_SetAllocTrackerActivationThreshold(parser, 999) == XML_TRUE); + assert_true(expat_malloc(parser, 1000, -1) == NULL); +#endif + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_alloc_tracker_getbuffer_unlimited) { + XML_Parser parser = XML_ParserCreate(NULL); + +#if XML_GE == 1 + // Artificially lower threshold + assert_true(XML_SetAllocTrackerActivationThreshold(parser, 0) == XML_TRUE); + + // Self-test: Prove that threshold is as rejecting as expected + assert_true(expat_malloc(parser, 1000, -1) == NULL); +#endif + // XML_GetBuffer should be allowed to pass, though + assert_true(XML_GetBuffer(parser, 1000) != NULL); + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_alloc_tracker_api) { + XML_Parser parserWithoutParent = XML_ParserCreate(NULL); + XML_Parser parserWithParent = XML_ExternalEntityParserCreate( + parserWithoutParent, XCS("entity123"), NULL); + if (parserWithoutParent == NULL) + fail("parserWithoutParent is NULL"); + if (parserWithParent == NULL) + fail("parserWithParent is NULL"); + +#if XML_GE == 1 + // XML_SetAllocTrackerMaximumAmplification, error cases + if (XML_SetAllocTrackerMaximumAmplification(NULL, 123.0f) == XML_TRUE) + fail("Call with NULL parser is NOT supposed to succeed"); + if (XML_SetAllocTrackerMaximumAmplification(parserWithParent, 123.0f) + == XML_TRUE) + fail("Call with non-root parser is NOT supposed to succeed"); + if (XML_SetAllocTrackerMaximumAmplification(parserWithoutParent, NAN) + == XML_TRUE) + fail("Call with NaN limit is NOT supposed to succeed"); + if (XML_SetAllocTrackerMaximumAmplification(parserWithoutParent, -1.0f) + == XML_TRUE) + fail("Call with negative limit is NOT supposed to succeed"); + if (XML_SetAllocTrackerMaximumAmplification(parserWithoutParent, 0.9f) + == XML_TRUE) + fail("Call with positive limit <1.0 is NOT supposed to succeed"); + + // XML_SetAllocTrackerMaximumAmplification, success cases + if (XML_SetAllocTrackerMaximumAmplification(parserWithoutParent, 1.0f) + == XML_FALSE) + fail("Call with positive limit >=1.0 is supposed to succeed"); + if (XML_SetAllocTrackerMaximumAmplification(parserWithoutParent, 123456.789f) + == XML_FALSE) + fail("Call with positive limit >=1.0 is supposed to succeed"); + if (XML_SetAllocTrackerMaximumAmplification(parserWithoutParent, INFINITY) + == XML_FALSE) + fail("Call with positive limit >=1.0 is supposed to succeed"); + + // XML_SetAllocTrackerActivationThreshold, error cases + if (XML_SetAllocTrackerActivationThreshold(NULL, 123) == XML_TRUE) + fail("Call with NULL parser is NOT supposed to succeed"); + if (XML_SetAllocTrackerActivationThreshold(parserWithParent, 123) == XML_TRUE) + fail("Call with non-root parser is NOT supposed to succeed"); + + // XML_SetAllocTrackerActivationThreshold, success cases + if (XML_SetAllocTrackerActivationThreshold(parserWithoutParent, 123) + == XML_FALSE) + fail("Call with non-NULL parentless parser is supposed to succeed"); +#endif // XML_GE == 1 + + XML_ParserFree(parserWithParent); + XML_ParserFree(parserWithoutParent); +} +END_TEST + +START_TEST(test_mem_api_cycle) { + XML_Parser parser = XML_ParserCreate(NULL); + + void *ptr = XML_MemMalloc(parser, 10); + + assert_true(ptr != NULL); + memset(ptr, 'x', 10); // assert writability, with ASan in mind + + ptr = XML_MemRealloc(parser, ptr, 20); + + assert_true(ptr != NULL); + memset(ptr, 'y', 20); // assert writability, with ASan in mind + + XML_MemFree(parser, ptr); + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_mem_api_unlimited) { + XML_Parser parser = XML_ParserCreate(NULL); + +#if XML_GE == 1 + assert_true(XML_SetAllocTrackerActivationThreshold(parser, 0) == XML_TRUE); +#endif + + void *ptr = XML_MemMalloc(parser, 1000); + + assert_true(ptr != NULL); + + ptr = XML_MemRealloc(parser, ptr, 2000); + + assert_true(ptr != NULL); + + XML_MemFree(parser, ptr); + + XML_ParserFree(parser); +} +END_TEST + void make_alloc_test_case(Suite *s) { TCase *tc_alloc = tcase_create("allocation tests"); @@ -2151,4 +2354,15 @@ make_alloc_test_case(Suite *s) { tcase_add_test__ifdef_xml_dtd( tc_alloc, test_alloc_reset_after_external_entity_parser_create_fail); + + tcase_add_test__ifdef_xml_dtd(tc_alloc, test_alloc_tracker_size_recorded); + tcase_add_test__ifdef_xml_dtd(tc_alloc, + test_alloc_tracker_maximum_amplification); + tcase_add_test__ifdef_xml_dtd(tc_alloc, test_alloc_tracker_threshold); + tcase_add_test__ifdef_xml_dtd(tc_alloc, + test_alloc_tracker_getbuffer_unlimited); + tcase_add_test__ifdef_xml_dtd(tc_alloc, test_alloc_tracker_api); + + tcase_add_test(tc_alloc, test_mem_api_cycle); + tcase_add_test__ifdef_xml_dtd(tc_alloc, test_mem_api_unlimited); }