From 31f9053c3c46741f4daf2ea2bdea75f40f720d42 Mon Sep 17 00:00:00 2001
From: Sebastian Pipping <sebastian@pipping.org>
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 <peter.marko@siemens.com>
---
 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 <math.h> /* NAN, INFINITY */
+#include <stdbool.h>
+#include <stdint.h> /* for SIZE_MAX */
 #include <string.h>
 #include <assert.h>
 
+#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 = "<e>";
+  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);
 }
