Commit 28fcfd2624 for freeswitch.com
commit 28fcfd26246c112fa4b9e698433d09e4836ca1b5
Author: Dmitry Verenitsin <morbit85@gmail.com>
Date: Sat Aug 8 23:02:15 2026 +0500
Merge commit from fork
* Merge commit from fork
* [core] Fix XML escape encoder overrun and unsigned-char UTF-8 gate
`switch_xml_ampencode()` had two independent defects in its UTF-8
numeric-escape path.
Buffer overrun: the encoder grows its destination once per source byte,
but the realloc margin reserved only the 10 data characters of the
widest escape `"&#x%X;"` (a 21-bit code point rendered as 6 hex digits),
not the terminating NUL that `sprintf` also writes. At the margin
boundary that NUL landed one byte past the allocation. Reserve 11 bytes
in the guard (10 data chars plus the NUL) and emit the escape with
`snprintf` bounded to the remaining space, so the write stays in bounds
even if the margin is ever miscounted. The other escape sinks are all
within the widened margin and are unchanged.
Char signedness: the lead-byte test `(*s >> 8) & 0x01` reads bit 8 of a
plain `char`, which exists only after sign extension. Where `char` is
signed the high bit sign-extends and the test passes; where `char` is
unsigned it is always zero, so the numeric-escape path never ran and
multi-byte UTF-8 was emitted raw, making serialized XML differ by
architecture. Test bit 7 directly with `(*s & 0x80)`, correct regardless
of `char` signedness. This also makes the overrun fix effective on
unsigned-`char` builds, where the escape path now runs.
Add unit test `test_utf_8_wide_codepoint`, which serializes U+10FFFF and
long runs of it across buffer reallocations, sweeping an ASCII prefix so
an escape is emitted at the minimum-headroom offset, and asserts the
full serialized length.
diff --git a/src/switch_xml.c b/src/switch_xml.c
index e92134d522..a52f94a7ae 100644
--- a/src/switch_xml.c
+++ b/src/switch_xml.c
@@ -2567,7 +2567,10 @@ static char *switch_xml_ampencode(const char *s, switch_size_t len, char **dst,
}
while (s != e) {
- while (*dlen + 10 > *max) {
+ /* Reserve room for the widest single-iteration output: "&#x%X;" is up to
+ 10 chars for a 21-bit code point, and sprintf/snprintf also write a
+ terminating NUL, so the worst case is 11 bytes. */
+ while (*dlen + 11 > *max) {
*dst = (char *) switch_must_realloc(*dst, *max += SWITCH_XML_BUFSIZE);
}
@@ -2607,7 +2610,7 @@ static char *switch_xml_ampencode(const char *s, switch_size_t len, char **dst,
*dlen += sprintf(*dst + *dlen, "
");
break;
default:
- if (use_utf8_encoding && expecting_x_utf_8_char == 0 && ((*s >> 8) & 0x01)) {
+ if (use_utf8_encoding && expecting_x_utf_8_char == 0 && (*s & 0x80)) {
int num = 1;
for (;num<4;num++) {
if (! ((*s >> (7-num)) & 0x01)) {
@@ -2643,7 +2646,7 @@ static char *switch_xml_ampencode(const char *s, switch_size_t len, char **dst,
}
expecting_x_utf_8_char--;
if (expecting_x_utf_8_char == 0) {
- *dlen += sprintf(*dst + *dlen, "&#x%X;", unicode_char);
+ *dlen += snprintf(*dst + *dlen, *max - *dlen, "&#x%X;", unicode_char);
}
} else {
(*dst)[(*dlen)++] = *s;
diff --git a/tests/unit/switch_xml.c b/tests/unit/switch_xml.c
index c1a5559872..6209200052 100644
--- a/tests/unit/switch_xml.c
+++ b/tests/unit/switch_xml.c
@@ -281,6 +281,91 @@ test_empty_entity_decode_done:
if (xml) switch_xml_free(xml);
}
FST_TEST_END()
+
+ FST_TEST_BEGIN(test_utf_8_wide_codepoint)
+ {
+ /* U+10FFFF is the largest Unicode code point; its UTF-8 form
+ F4 8F BF BF serializes to "", the widest &#x...;
+ escape (10 chars) the encoder emits. It must serialize intact
+ and must not overrun the destination buffer. */
+ const char *single = "<xml>\xF4\x8F\xBF\xBF" "</xml>";
+ switch_xml_t xml = NULL;
+ char *xml_string = NULL;
+ int prefix;
+
+ xml = switch_xml_parse_str_dynamic((char *)single, SWITCH_TRUE);
+ if (!xml) {
+ fst_fail("failed to parse maximum code point document");
+ goto test_utf_8_wide_done;
+ }
+
+ xml_string = switch_xml_toxml(xml, SWITCH_FALSE);
+ if (!xml_string) {
+ fst_fail("failed to serialize maximum code point");
+ goto test_utf_8_wide_done;
+ }
+
+ fst_check_string_equals(xml_string, "<xml></xml>\n");
+ free(xml_string);
+ xml_string = NULL;
+ switch_xml_free(xml);
+ xml = NULL;
+
+ /* Serialize long runs of the widest escape so the destination
+ buffer reallocates many times. Sweeping the ASCII prefix length
+ shifts the write offset so that, across iterations, an escape is
+ emitted at every alignment relative to the reserved headroom,
+ including the tightest one. Each run must serialize intact: the
+ full escaped length, with no truncation or dropped escape. */
+ for (prefix = 0; prefix < 10; prefix++) {
+ switch_size_t runs = 1100;
+ switch_size_t cap = 5 + prefix + runs * 4 + 6 + 1;
+ char *doc = switch_must_malloc(cap);
+ char *w = doc;
+ switch_size_t i;
+ switch_size_t expected_len = 5 + (switch_size_t) prefix + runs * 10 + 7;
+
+ memcpy(w, "<xml>", 5);
+ w += 5;
+ for (i = 0; i < (switch_size_t) prefix; i++) {
+ *w++ = 'a';
+ }
+ for (i = 0; i < runs; i++) {
+ *w++ = (char) 0xF4;
+ *w++ = (char) 0x8F;
+ *w++ = (char) 0xBF;
+ *w++ = (char) 0xBF;
+ }
+ memcpy(w, "</xml>", 6);
+ w += 6;
+ *w = '\0';
+
+ xml = switch_xml_parse_str_dynamic(doc, SWITCH_TRUE);
+ free(doc);
+ if (!xml) {
+ fst_fail("failed to parse long wide-escape run");
+ goto test_utf_8_wide_done;
+ }
+
+ xml_string = switch_xml_toxml(xml, SWITCH_FALSE);
+ if (!xml_string) {
+ fst_fail("failed to serialize long wide-escape run");
+ goto test_utf_8_wide_done;
+ }
+
+ fst_check_string_has(xml_string, "");
+ fst_xcheck(strlen(xml_string) == expected_len, "serialized wide-escape run has wrong length");
+ free(xml_string);
+ xml_string = NULL;
+ switch_xml_free(xml);
+ xml = NULL;
+ }
+
+test_utf_8_wide_done:
+ free(xml_string);
+ switch_xml_free(xml);
+ }
+ FST_TEST_END()
}
FST_SUITE_END()
}