Commit 394b48b9f5 for freeswitch.com

commit 394b48b9f5ff44497da03921cb871d1232478b99
Author: Dmitry Verenitsin <morbit85@gmail.com>
Date:   Sat Aug 8 23:40:30 2026 +0500

    [core] Harden switch_b64_decode output bound and input handling (#3110)

    switch_b64_decode bounded its writes with `ol >= olen - 1`, where
    `olen` is unsigned. An `olen` of 0 made `olen - 1` wrap to `SIZE_MAX`,
    so the bound never fired and the loop wrote the entire decoded input
    plus a trailing NUL past the destination. Reject `olen == 0` and test
    `ol + 1 >= olen` before each write, so the comparison never subtracts
    from an unsigned and always leaves room for the terminator.

    The alphabet lookup table `l64` was a `char` indexed by a `char`,
    which is unsafe whichever way `char` is signed:

    - Where `char` is signed, an input byte >= 0x80 became a negative
      index and read before the table.
    - Where `char` is unsigned, the `-1` "not in alphabet" sentinel was
      stored as 255, so the skip test never matched and non-alphabet
      bytes were folded in as data.

    Make `l64` a `signed char` and index it with `(unsigned char)`, so the
    sentinel survives and the index stays in range on every platform.

    Change the bit accumulator `b` from `int` to `unsigned int` to avoid
    signed-overflow undefined behavior on long input; decoded output is
    unchanged.

    Add unit tests: an encode/decode round-trip across the padding cases,
    the output-bound edges (`olen` of 0, 1, and a truncating buffer), and
    a non-alphabet byte (including one >= 0x80) that must be skipped.

diff --git a/src/switch_utils.c b/src/switch_utils.c
index 854659ab4a..ad0f96b70c 100644
--- a/src/switch_utils.c
+++ b/src/switch_utils.c
@@ -1062,22 +1062,27 @@ SWITCH_DECLARE(switch_status_t) switch_b64_encode(unsigned char *in, switch_size
 SWITCH_DECLARE(switch_size_t) switch_b64_decode(const char *in, char *out, switch_size_t olen)
 {

-	char l64[256];
-	int b = 0, c, l = 0, i;
+	signed char l64[256];
+	int c, l = 0, i;
+	unsigned int b = 0;
 	const char *ip;
 	char *op = out;
 	size_t ol = 0;

+	if (olen == 0) {	/* no room even for the trailing NUL */
+		return 0;
+	}
+
 	for (i = 0; i < 256; i++) {
 		l64[i] = -1;
 	}

 	for (i = 0; i < 64; i++) {
-		l64[(int) switch_b64_table[i]] = (char) i;
+		l64[(unsigned char) switch_b64_table[i]] = (signed char) i;
 	}

 	for (ip = in; ip && *ip && (*ip != '='); ip++) {
-		c = l64[(int) *ip];
+		c = l64[(unsigned char) *ip];
 		if (c == -1) {
 			continue;
 		}
@@ -1086,10 +1091,10 @@ SWITCH_DECLARE(switch_size_t) switch_b64_decode(const char *in, char *out, switc
 		l += 6;

 		while (l >= 8) {
-			op[ol++] = (char) ((b >> (l -= 8)) % 256);
-			if (ol >= olen - 1) {
+			if (ol + 1 >= olen) {	/* reserve the last byte for the NUL */
 				goto end;
 			}
+			op[ol++] = (char) ((b >> (l -= 8)) % 256);
 		}
 	}

diff --git a/tests/unit/switch_utils.c b/tests/unit/switch_utils.c
index 959f9f2e4f..a883c4602c 100644
--- a/tests/unit/switch_utils.c
+++ b/tests/unit/switch_utils.c
@@ -124,6 +124,93 @@ FST_TEST_BEGIN(b64_pad1)
 }
 FST_TEST_END()

+FST_TEST_BEGIN(b64_roundtrip)
+{
+	/* Encode then decode inputs covering all three padding cases; the base64 output must
+	   match the known value and decode back to the original bytes. Unlike b64_pad1/b64_pad2
+	   (all-zero input), these push non-zero bytes through the padded final group. */
+	struct {
+		const char *plain;
+		const char *encoded;
+	} cases[] = {
+		{ "Man",           "TWFu" },					/* no padding */
+		{ "Ma",            "TWE=" },					/* one pad byte */
+		{ "M",             "TQ==" },					/* two pad bytes */
+		{ "Hello, World!", "SGVsbG8sIFdvcmxkIQ==" }
+	};
+	int i;
+
+	for (i = 0; i < (int) (sizeof(cases) / sizeof(cases[0])); i++) {
+		unsigned char encoded[64];
+		char decoded[64];
+		switch_size_t plain_len = strlen(cases[i].plain);
+		switch_size_t decoded_len;
+		switch_status_t status = switch_b64_encode((unsigned char *) cases[i].plain, plain_len, encoded, sizeof(encoded));
+
+		fst_xcheck(status == SWITCH_STATUS_SUCCESS, "encode must succeed");
+		fst_check_string_equals((const char *) encoded, cases[i].encoded);
+
+		decoded_len = switch_b64_decode((const char *) encoded, decoded, sizeof(decoded));
+		fst_xcheck(decoded_len == plain_len + 1, "decode must return the plaintext length plus the trailing NUL");
+		fst_check_string_equals(decoded, cases[i].plain);
+	}
+}
+FST_TEST_END()
+
+FST_TEST_BEGIN(b64_decode_output_bounds)
+{
+	/* The 0xAA sentinel across the destination catches any write outside the region
+	   the decode call is allowed to touch. */
+	unsigned char guarded[32];
+	switch_size_t size;
+	int i;
+
+	/* Decode with olen == 0: no room even for the trailing NUL, so the decoder must
+	   write nothing and return 0. */
+	memset(guarded, 0xAA, sizeof(guarded));
+	size = switch_b64_decode("QUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJD", (char *) guarded, 0);
+	fst_xcheck(size == 0, "olen==0 decode must return 0");
+	for (i = 0; i < (int) sizeof(guarded); i++) {
+		fst_xcheck(guarded[i] == 0xAA, "olen==0 decode must not write any output byte");
+	}
+
+	/* Decode with olen == 1: room only for the terminating NUL at index 0; no decoded
+	   data byte may be written. */
+	memset(guarded, 0xAA, sizeof(guarded));
+	size = switch_b64_decode("QUJDQUJDQUJDQUJD", (char *) guarded, 1);
+	fst_xcheck(size == 1, "olen==1 decode must return 1 (NUL only)");
+	fst_xcheck(guarded[0] == '\0', "olen==1 decode must store the NUL at index 0");
+	for (i = 1; i < (int) sizeof(guarded); i++) {
+		fst_xcheck(guarded[i] == 0xAA, "olen==1 decode must not write past index 0");
+	}
+
+	/* Decode with a small olen: up to olen-1 decoded bytes, then the trailing NUL at
+	   index olen-1, and nothing beyond. "QUJD" decodes to "ABC". */
+	memset(guarded, 0xAA, sizeof(guarded));
+	size = switch_b64_decode("QUJD", (char *) guarded, 2);
+	fst_xcheck(size == 2, "bounded decode must return olen");
+	fst_xcheck(guarded[0] == 'A', "first decoded byte must be written");
+	fst_xcheck(guarded[1] == '\0', "trailing NUL must be at index olen-1");
+	for (i = 2; i < (int) sizeof(guarded); i++) {
+		fst_xcheck(guarded[i] == 0xAA, "bounded decode must not write past index olen-1");
+	}
+}
+FST_TEST_END()
+
+FST_TEST_BEGIN(b64_decode_non_alphabet_bytes)
+{
+	/* Bytes outside the base64 alphabet, including those >= 0x80, are skipped and never used
+	   as a lookup-table index. */
+	char decoded[8];
+	switch_size_t size;
+
+	/* "QUJD" ("ABC") with a non-alphabet 0x80 byte spliced in. */
+	size = switch_b64_decode("QU\x80" "JD", decoded, sizeof(decoded));
+	fst_xcheck(size == 4, "non-alphabet byte must be skipped, leaving 3 data bytes plus the NUL");
+	fst_check_string_equals(decoded, "ABC");
+}
+FST_TEST_END()
+
 #define test_uri_count 6

 /* Currently tests only clear_uri() */