From 81a114f7eebcd41a6993337128cda337986a26f4 Mon Sep 17 00:00:00 2001
From: Sebastian Pipping <sebastian@pipping.org>
Date: Mon, 15 Sep 2025 21:57:07 +0200
Subject: [PATCH] tests: Cover XML_ERROR_ASYNC_ENTITY cases

CVE: CVE-2024-8176
Upstream-Status: Backport [https://github.com/libexpat/libexpat/pull/1059]
Signed-off-by: Peter Marko <peter.marko@siemens.com>
---
 tests/misc_tests.c | 87 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 87 insertions(+)

diff --git a/tests/misc_tests.c b/tests/misc_tests.c
index 3346bce6..19f41df7 100644
--- a/tests/misc_tests.c
+++ b/tests/misc_tests.c
@@ -621,6 +621,91 @@ START_TEST(test_misc_expected_event_ptr_issue_980) {
 }
 END_TEST
 
+START_TEST(test_misc_sync_entity_tolerated) {
+  const char *const doc = "<!DOCTYPE t0 [\n"
+                          "   <!ENTITY a '<t1></t1>'>\n"
+                          "   <!ENTITY b '<t2>two</t2>'>\n"
+                          "   <!ENTITY c '<t3>three<t4>four</t4>three</t3>'>\n"
+                          "   <!ENTITY d '<t5>&b;</t5>'>\n"
+                          "]>\n"
+                          "<t0>&a;&b;&c;&d;</t0>\n";
+  XML_Parser parser = XML_ParserCreate(NULL);
+
+  assert_true(_XML_Parse_SINGLE_BYTES(parser, doc, (int)strlen(doc),
+                                      /*isFinal=*/XML_TRUE)
+              == XML_STATUS_OK);
+
+  XML_ParserFree(parser);
+}
+END_TEST
+
+START_TEST(test_misc_async_entity_rejected) {
+  struct test_case {
+    const char *doc;
+    enum XML_Status expectedStatusNoGE;
+    enum XML_Error expectedErrorNoGE;
+  };
+  const struct test_case cases[] = {
+      // Opened by one entity, closed by another
+      {"<!DOCTYPE t0 [\n"
+       "   <!ENTITY open '<t1>'>\n"
+       "   <!ENTITY close '</t1>'>\n"
+       "]>\n"
+       "<t0>&open;&close;</t0>\n",
+       XML_STATUS_OK, XML_ERROR_NONE},
+      // Opened by tag, closed by entity (non-root case)
+      {"<!DOCTYPE t0 [\n"
+       "  <!ENTITY g0 ''>\n"
+       "  <!ENTITY g1 '&g0;</t1>'>\n"
+       "]>\n"
+       "<t0><t1>&g1;</t0>\n",
+       XML_STATUS_ERROR, XML_ERROR_TAG_MISMATCH},
+      // Opened by tag, closed by entity (root case)
+      {"<!DOCTYPE t0 [\n"
+       "  <!ENTITY g0 ''>\n"
+       "  <!ENTITY g1 '&g0;</t0>'>\n"
+       "]>\n"
+       "<t0>&g1;\n",
+       XML_STATUS_ERROR, XML_ERROR_NO_ELEMENTS},
+      // Opened by entity, closed by tag <-- regression from 2.7.0
+      {"<!DOCTYPE t0 [\n"
+       "  <!ENTITY g0 ''>\n"
+       "  <!ENTITY g1 '<t1>&g0;'>\n"
+       "]>\n"
+       "<t0>&g1;</t1></t0>\n",
+       XML_STATUS_ERROR, XML_ERROR_TAG_MISMATCH},
+      // Opened by tag, closed by entity; then the other way around
+      {"<!DOCTYPE t0 [\n"
+       "  <!ENTITY open '<t1>'>\n"
+       "  <!ENTITY close '</t1>'>\n"
+       "]>\n"
+       "<t0><t1>&close;&open;</t1></t0>\n",
+       XML_STATUS_OK, XML_ERROR_NONE},
+  };
+
+  for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
+    const struct test_case testCase = cases[i];
+    set_subtest("cases[%d]", (int)i);
+
+    const char *const doc = testCase.doc;
+#if XML_GE == 1
+    const enum XML_Status expectedStatus = XML_STATUS_ERROR;
+    const enum XML_Error expectedError = XML_ERROR_ASYNC_ENTITY;
+#else
+    const enum XML_Status expectedStatus = testCase.expectedStatusNoGE;
+    const enum XML_Error expectedError = testCase.expectedErrorNoGE;
+#endif
+
+    XML_Parser parser = XML_ParserCreate(NULL);
+    assert_true(_XML_Parse_SINGLE_BYTES(parser, doc, (int)strlen(doc),
+                                        /*isFinal=*/XML_TRUE)
+                == expectedStatus);
+    assert_true(XML_GetErrorCode(parser) == expectedError);
+    XML_ParserFree(parser);
+  }
+}
+END_TEST
+
 void
 make_miscellaneous_test_case(Suite *s) {
   TCase *tc_misc = tcase_create("miscellaneous tests");
@@ -649,4 +734,6 @@ make_miscellaneous_test_case(Suite *s) {
   tcase_add_test(tc_misc, test_misc_stopparser_rejects_unstarted_parser);
   tcase_add_test__if_xml_ge(tc_misc, test_renter_loop_finite_content);
   tcase_add_test(tc_misc, test_misc_expected_event_ptr_issue_980);
+  tcase_add_test(tc_misc, test_misc_sync_entity_tolerated);
+  tcase_add_test(tc_misc, test_misc_async_entity_rejected);
 }
