untrusted comment: verify with openbsd-78-base.pub
RWS3/nvFmk4SWZ4xz+JrQHj9xYKXtG74q5dSx2y18vB5B4FhxTeeG8OYwsBEYd8uxnlZhWu5vzfDBcm0qBjQC8/6lgBfDcmRiQI=

OpenBSD 7.8 errata 024, March 21, 2026:

In libexpat fix denial of service due to NULL dereference and
infinite loop.  CVE-2026-32776 CVE-2026-32777 CVE-2026-32778

Apply by doing:
    signify -Vep /etc/signify/openbsd-78-base.pub -x 024_expat.patch.sig \
        -m - | (cd /usr/src && patch -p0)

And then rebuild and install libexpat:
    cd /usr/src/lib/libexpat
    make obj
    make
    make install

Index: lib/libexpat/Changes
===================================================================
RCS file: /cvs/src/lib/libexpat/Changes,v
diff -u -p -r1.32.2.1 Changes
--- lib/libexpat/Changes	6 Feb 2026 21:26:12 -0000	1.32.2.1
+++ lib/libexpat/Changes	18 Mar 2026 18:09:32 -0000
@@ -42,6 +42,27 @@
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 
         Security fixes:
+           #1158  CVE-2026-32776 -- Fix NULL function pointer dereference for
+                    empty external parameter entities; it takes use of both
+                    functions XML_ExternalEntityParserCreate and
+                    XML_SetParamEntityParsing for an application to be
+                    vulnerable.
+     #1161 #1162  CVE-2026-32777 -- Protect from XML_TOK_INSTANCE_START
+                    infinite loop in function entityValueProcessor; it takes
+                    use of both functions XML_ExternalEntityParserCreate and
+                    XML_SetParamEntityParsing for an application to be
+                    vulnerable.
+           #1163  CVE-2026-32778 -- Fix NULL dereference in function setContext
+                    on retry after an earlier ouf-of-memory condition; it takes
+                    use of function XML_ParserCreateNS or XML_ParserCreate_MM
+                    for an application to be vulnerable.
+
+        Other changes:
+           #1156  Address Cppcheck >=2.20.0 warnings
+           #1153  tests: Make test_buffer_can_grow_to_max work for MinGW on
+                    Ubuntu 24.04
+
+        Security fixes:
            #1131  CVE-2026-24515 -- Function XML_ExternalEntityParserCreate
                     failed to copy the encoding handler data passed to
                     XML_SetUnknownEncodingHandler from the parent to the new
Index: lib/libexpat/lib/xmlparse.c
===================================================================
RCS file: /cvs/src/lib/libexpat/lib/xmlparse.c,v
diff -u -p -r1.44.2.1 xmlparse.c
--- lib/libexpat/lib/xmlparse.c	6 Feb 2026 21:26:12 -0000	1.44.2.1
+++ lib/libexpat/lib/xmlparse.c	18 Mar 2026 18:09:33 -0000
@@ -590,6 +590,8 @@ static XML_Char *poolStoreString(STRING_
 static XML_Bool FASTCALL poolGrow(STRING_POOL *pool);
 static const XML_Char *FASTCALL poolCopyString(STRING_POOL *pool,
                                                const XML_Char *s);
+static const XML_Char *FASTCALL poolCopyStringNoFinish(STRING_POOL *pool,
+                                                       const XML_Char *s);
 static const XML_Char *poolCopyStringN(STRING_POOL *pool, const XML_Char *s,
                                        int n);
 static const XML_Char *FASTCALL poolAppendString(STRING_POOL *pool,
@@ -5086,7 +5088,7 @@ entityValueInitProcessor(XML_Parser pars
     }
     /* If we get this token, we have the start of what might be a
        normal tag, but not a declaration (i.e. it doesn't begin with
-       "<!").  In a DTD context, that isn't legal.
+       "<!" or "<?").  In a DTD context, that isn't legal.
     */
     else if (tok == XML_TOK_INSTANCE_START) {
       *nextPtr = next;
@@ -5175,6 +5177,15 @@ entityValueProcessor(XML_Parser parser, 
       /* found end of entity value - can store it now */
       return storeEntityValue(parser, enc, s, end, XML_ACCOUNT_DIRECT, NULL);
     }
+    /* If we get this token, we have the start of what might be a
+       normal tag, but not a declaration (i.e. it doesn't begin with
+       "<!" or "<?").  In a DTD context, that isn't legal.
+    */
+    else if (tok == XML_TOK_INSTANCE_START) {
+      *nextPtr = next;
+      return XML_ERROR_SYNTAX;
+    }
+
     start = next;
   }
 }
@@ -6789,7 +6800,14 @@ storeEntityValue(XML_Parser parser, cons
       return XML_ERROR_NO_MEMORY;
   }
 
-  const char *next;
+  const char *next = entityTextPtr;
+
+  /* Nothing to tokenize. */
+  if (entityTextPtr >= entityTextEnd) {
+    result = XML_ERROR_NONE;
+    goto endEntityValue;
+  }
+
   for (;;) {
     next
         = entityTextPtr; /* XmlEntityValueTok doesn't always set the last arg */
@@ -7439,16 +7457,24 @@ setContext(XML_Parser parser, const XML_
       else {
         if (! poolAppendChar(&parser->m_tempPool, XML_T('\0')))
           return XML_FALSE;
-        prefix
-            = (PREFIX *)lookup(parser, &dtd->prefixes,
-                               poolStart(&parser->m_tempPool), sizeof(PREFIX));
-        if (! prefix)
+        const XML_Char *const prefixName = poolCopyStringNoFinish(
+            &dtd->pool, poolStart(&parser->m_tempPool));
+        if (! prefixName) {
           return XML_FALSE;
-        if (prefix->name == poolStart(&parser->m_tempPool)) {
-          prefix->name = poolCopyString(&dtd->pool, prefix->name);
-          if (! prefix->name)
-            return XML_FALSE;
         }
+
+        prefix = (PREFIX *)lookup(parser, &dtd->prefixes, prefixName,
+                                  sizeof(PREFIX));
+
+        const bool prefixNameUsed = prefix && prefix->name == prefixName;
+        if (prefixNameUsed)
+          poolFinish(&dtd->pool);
+        else
+          poolDiscard(&dtd->pool);
+
+        if (! prefix)
+          return XML_FALSE;
+
         poolDiscard(&parser->m_tempPool);
       }
       for (context = s + 1; *context != CONTEXT_SEP && *context != XML_T('\0');
@@ -8034,6 +8060,23 @@ poolCopyString(STRING_POOL *pool, const 
   s = pool->start;
   poolFinish(pool);
   return s;
+}
+
+// A version of `poolCopyString` that does not call `poolFinish`
+// and reverts any partial advancement upon failure.
+static const XML_Char *FASTCALL
+poolCopyStringNoFinish(STRING_POOL *pool, const XML_Char *s) {
+  const XML_Char *const original = s;
+  do {
+    if (! poolAppendChar(pool, *s)) {
+      // Revert any previously successful advancement
+      const ptrdiff_t advancedBy = s - original;
+      if (advancedBy > 0)
+        pool->ptr -= advancedBy;
+      return NULL;
+    }
+  } while (*s++);
+  return pool->start;
 }
 
 static const XML_Char *
Index: lib/libexpat/tests/basic_tests.c
===================================================================
RCS file: /cvs/src/lib/libexpat/tests/basic_tests.c,v
diff -u -p -r1.7.2.1 basic_tests.c
--- lib/libexpat/tests/basic_tests.c	6 Feb 2026 21:26:12 -0000	1.7.2.1
+++ lib/libexpat/tests/basic_tests.c	18 Mar 2026 18:09:33 -0000
@@ -3112,12 +3112,16 @@ START_TEST(test_buffer_can_grow_to_max) 
 #if defined(__MINGW32__) && ! defined(__MINGW64__)
   // workaround for mingw/wine32 on GitHub CI not being able to reach 1GiB
   // Can we make a big allocation?
-  void *big = malloc(maxbuf);
-  if (! big) {
+  for (int i = 1; i <= 2; i++) {
+    void *const big = malloc(maxbuf);
+    if (big != NULL) {
+      free(big);
+      break;
+    }
     // The big allocation failed. Let's be a little lenient.
     maxbuf = maxbuf / 2;
+    fprintf(stderr, "Reducing maxbuf to %d...\n", maxbuf);
   }
-  free(big);
 #endif
 
   for (int i = 0; i < num_prefixes; ++i) {
@@ -6043,6 +6047,7 @@ START_TEST(test_bypass_heuristic_when_cl
 
   const int document_length = 65536;
   char *const document = (char *)malloc(document_length);
+  assert_true(document != NULL);
 
   const XML_Memory_Handling_Suite memfuncs = {
       counting_malloc,
@@ -6257,6 +6262,24 @@ START_TEST(test_varying_buffer_fills) {
 END_TEST
 #endif
 
+START_TEST(test_empty_ext_param_entity_in_value) {
+  const char *text = "<!DOCTYPE r SYSTEM \"ext.dtd\"><r/>";
+  ExtOption options[] = {
+      {XCS("ext.dtd"), "<!ENTITY % pe SYSTEM \"empty\">"
+                       "<!ENTITY ge \"%pe;\">"},
+      {XCS("empty"), ""},
+      {NULL, NULL},
+  };
+
+  XML_SetParamEntityParsing(g_parser, XML_PARAM_ENTITY_PARSING_ALWAYS);
+  XML_SetExternalEntityRefHandler(g_parser, external_entity_optioner);
+  XML_SetUserData(g_parser, options);
+  if (_XML_Parse_SINGLE_BYTES(g_parser, text, (int)strlen(text), XML_TRUE)
+      == XML_STATUS_ERROR)
+    xml_failure(g_parser);
+}
+END_TEST
+
 void
 make_basic_test_case(Suite *s) {
   TCase *tc_basic = tcase_create("basic tests");
@@ -6504,6 +6527,7 @@ make_basic_test_case(Suite *s) {
   tcase_add_test(tc_basic, test_empty_element_abort);
   tcase_add_test__ifdef_xml_dtd(tc_basic,
                                 test_pool_integrity_with_unfinished_attr);
+  tcase_add_test__ifdef_xml_dtd(tc_basic, test_empty_ext_param_entity_in_value);
   tcase_add_test__if_xml_ge(tc_basic, test_entity_ref_no_elements);
   tcase_add_test__if_xml_ge(tc_basic, test_deep_nested_entity);
   tcase_add_test__if_xml_ge(tc_basic, test_deep_nested_attribute_entity);
Index: lib/libexpat/tests/misc_tests.c
===================================================================
RCS file: /cvs/src/lib/libexpat/tests/misc_tests.c,v
diff -u -p -r1.8 misc_tests.c
--- lib/libexpat/tests/misc_tests.c	25 Sep 2025 19:05:10 -0000	1.8
+++ lib/libexpat/tests/misc_tests.c	18 Mar 2026 18:09:33 -0000
@@ -771,6 +771,35 @@ START_TEST(test_misc_async_entity_reject
 }
 END_TEST
 
+START_TEST(test_misc_no_infinite_loop_issue_1161) {
+  XML_Parser parser = XML_ParserCreate(NULL);
+
+  const char *text = "<!DOCTYPE d SYSTEM 'secondary.txt'>";
+
+  struct ExtOption options[] = {
+      {XCS("secondary.txt"),
+       "<!ENTITY % p SYSTEM 'tertiary.txt'><!ENTITY g '%p;'>"},
+      {XCS("tertiary.txt"), "<?xml version='1.0'?><a"},
+      {NULL, NULL},
+  };
+
+  XML_SetUserData(parser, options);
+  XML_SetParamEntityParsing(parser, XML_PARAM_ENTITY_PARSING_ALWAYS);
+  XML_SetExternalEntityRefHandler(parser, external_entity_optioner);
+
+  assert_true(_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE)
+              == XML_STATUS_ERROR);
+
+#if defined(XML_DTD)
+  assert_true(XML_GetErrorCode(parser) == XML_ERROR_EXTERNAL_ENTITY_HANDLING);
+#else
+  assert_true(XML_GetErrorCode(parser) == XML_ERROR_NO_ELEMENTS);
+#endif
+
+  XML_ParserFree(parser);
+}
+END_TEST
+
 void
 make_miscellaneous_test_case(Suite *s) {
   TCase *tc_misc = tcase_create("miscellaneous tests");
@@ -801,4 +830,5 @@ make_miscellaneous_test_case(Suite *s) {
   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);
+  tcase_add_test(tc_misc, test_misc_no_infinite_loop_issue_1161);
 }
Index: lib/libexpat/tests/nsalloc_tests.c
===================================================================
RCS file: /cvs/src/lib/libexpat/tests/nsalloc_tests.c,v
diff -u -p -r1.2 nsalloc_tests.c
--- lib/libexpat/tests/nsalloc_tests.c	17 Sep 2025 17:23:00 -0000	1.2
+++ lib/libexpat/tests/nsalloc_tests.c	18 Mar 2026 18:09:34 -0000
@@ -1505,6 +1505,32 @@ START_TEST(test_nsalloc_prefixed_element
 }
 END_TEST
 
+/* Verify that retry after OOM in setContext() does not crash.
+ */
+START_TEST(test_nsalloc_setContext_zombie) {
+  const char *text = "<doc>Hello</doc>";
+  unsigned int i;
+  const unsigned int max_alloc_count = 30;
+
+  for (i = 0; i < max_alloc_count; i++) {
+    g_allocation_count = (int)i;
+    if (XML_Parse(g_parser, text, (int)strlen(text), XML_TRUE)
+        != XML_STATUS_ERROR)
+      break;
+    /* Retry on the same parser — must not crash */
+    g_allocation_count = ALLOC_ALWAYS_SUCCEED;
+    XML_Parse(g_parser, text, (int)strlen(text), XML_TRUE);
+
+    nsalloc_teardown();
+    nsalloc_setup();
+  }
+  if (i == 0)
+    fail("Parsing worked despite failing allocations");
+  else if (i == max_alloc_count)
+    fail("Parsing failed even at maximum allocation count");
+}
+END_TEST
+
 void
 make_nsalloc_test_case(Suite *s) {
   TCase *tc_nsalloc = tcase_create("namespace allocation tests");
@@ -1539,4 +1565,5 @@ make_nsalloc_test_case(Suite *s) {
   tcase_add_test__if_xml_ge(tc_nsalloc, test_nsalloc_long_default_in_ext);
   tcase_add_test(tc_nsalloc, test_nsalloc_long_systemid_in_ext);
   tcase_add_test(tc_nsalloc, test_nsalloc_prefixed_element);
+  tcase_add_test(tc_nsalloc, test_nsalloc_setContext_zombie);
 }
