fix: dev: Detect UTF-16 surrogates in isc_utf8_valid()

UTF-8 standard forbid usage of unicode character between the range of
0xD800..0xDFFF (reserved, and used as UTF-16 surrogates, see RFC 3629).

However, `usc_utf8_valid()` was not checking if the encoded unicode
character was in this range, which then would accept invalid UTF-8
strings. This is now fixed.

Closes #6151

Merge branch '6151-utf16-surrogates-detection' into 'main'

See merge request isc-projects/bind9!12345
This commit is contained in:
Colin Vidal 2026-07-08 11:03:52 +02:00
commit dfefde8481
3 changed files with 202 additions and 0 deletions

View file

@ -33,9 +33,20 @@ isc_utf8_valid(const unsigned char *buf, size_t len) {
REQUIRE(buf != NULL);
for (size_t i = 0; i < len; i++) {
/*
* ASCII character range (first row).
*/
if (buf[i] <= 0x7f) {
continue;
}
/*
* 0x80 -> 1000 0000
* 0xC0 -> 1100 0000
* 0xE0 -> 1110 0000
*
* Is unicode character is encoded using 2 bytes (second row).
*/
if ((i + 1) < len && (buf[i] & 0xe0) == 0xc0 &&
(buf[i + 1] & 0xc0) == 0x80)
{
@ -47,6 +58,15 @@ isc_utf8_valid(const unsigned char *buf, size_t len) {
}
continue;
}
/*
* 0x80 -> 1000 0000
* 0xC0 -> 1100 0000
* 0xE0 -> 1110 0000
* 0xF0 -> 1111 0000
*
* Is unicode character is encoded within 3 bytes (third row).
*/
if ((i + 2) < len && (buf[i] & 0xf0) == 0xe0 &&
(buf[i + 1] & 0xc0) == 0x80 && (buf[i + 2] & 0xc0) == 0x80)
{
@ -57,8 +77,26 @@ isc_utf8_valid(const unsigned char *buf, size_t len) {
if (w < 0x0800) {
return false;
}
/*
* Unicode range 0xD800..0xDFFF is reserved (UTF16
* surrogates)
*/
if (w >= 0xD800 && w <= 0xDFFF) {
return false;
}
continue;
}
/*
* 0x80 -> 1000 0000
* 0xC0 -> 1100 0000
* 0xE0 -> 1110 0000
* 0xF0 -> 1111 0000
* 0xF8 -> 1111 1000
*
* Is unicode character is encoded within 4 bytes (fourth row).
*/
if ((i + 3) < len && (buf[i] & 0xf8) == 0xf0 &&
(buf[i + 1] & 0xc0) == 0x80 &&
(buf[i + 2] & 0xc0) == 0x80 && (buf[i + 3] & 0xc0) == 0x80)

View file

@ -54,6 +54,7 @@ isc_test = [
'tlsdns',
'udp',
'url',
'utf8',
'work',
]

163
tests/isc/utf8_test.c Normal file
View file

@ -0,0 +1,163 @@
/*
* Copyright (C) Internet Systems Consortium, Inc. ("ISC")
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* See the COPYRIGHT file distributed with this work for additional
* information regarding copyright ownership.
*/
#include <ctype.h>
#include <inttypes.h>
#include <sched.h> /* IWYU pragma: keep */
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
#define UNIT_TESTING
#include <cmocka.h>
#include <isc/lib.h>
#include <isc/utf8.h>
#include <tests/isc.h>
static void
valid(const char *str) {
assert_true(isc_utf8_valid((const unsigned char *)str, strlen(str)));
}
static void
invalid(const char *str) {
assert_false(isc_utf8_valid((const unsigned char *)str, strlen(str)));
}
static void
validbom(const char *str) {
assert_true(isc_utf8_bom((const unsigned char *)str, strlen(str)));
}
static void
invalidbom(const char *str) {
assert_false(isc_utf8_bom((const unsigned char *)str, strlen(str)));
}
ISC_RUN_TEST_IMPL(test_utf8_ascii) {
valid("#1@#'Been a long lonely, lonely, lonely, lonely, lonely time");
}
ISC_RUN_TEST_IMPL(test_utf8_twobytes) {
/*
* 0xC280 -> 1100 0010 1000 0000 (unicode character 0x80, below it's
* ASCII, so it would be invalid)
*
* 0xDFBF -> 1101 1111 1011 1111 (unicode character 0x07FF)
*/
invalid("invalid 2-bytes sequence: \xC0\x80 (invalid)");
valid("2-bytes sequence: \xC2\x80 (begining of the range)");
valid("2-bytes sequence: \xDF\xBF (end of range)");
invalid("invalid (second bytes doesn't starts with 10__) \xDF\xFF");
}
ISC_RUN_TEST_IMPL(test_utf8_threebytes) {
/*
* 0x0800 (first valid unicode character holding in 3 bytes in UTF-8) is
* 0000 |1000 00|00 0000 which encodes in
*
* 1110 ____ 10__ ____ 10__ ____
* 1110 0000 1010 0000 1000 0000
*
* So, 0xE0A080
*
* 0xEFBFBF is 1110 1111 1011 1111 1011 1111 which is the maximum
* unicode character encoded in 3-bytes in UTF-8.
*/
invalid("invalid 3-bytes sequence: \xE0\x80\x88");
invalid("invalid 3-bytes sequence: \xE0\x84\x80");
valid("valid 3-bytes sequence: \xE0\xA0\x80 (min value)");
valid("valid 3-bytes sequence: \xEF\xBF\xBF (max value)");
invalid("invalid 3-bytes sequence \xE0\xC1\xC1 (extra bytes does not "
"start with 10__)");
}
ISC_RUN_TEST_IMPL(test_utf8_utf16_surrogate) {
/*
* Right before the surrogate range.
*/
valid("a 3-bytes encoded UTF-8 character \xED\x90\x80 right before the "
"surrogate range");
/*
* 0xED 0xA0 0x80 sequence is UTF-8 encoding of 0xD800.
*/
invalid("a utf16 \xED\xA0\x80 test");
/*
* Within the surrogate range.
*/
invalid("a utf16 \xED\xA0\x88 test");
/*
* 0xED 0xBF 0xBF sequence is UTF-8 encoding of 0xDFFF.
*/
invalid("a utf16 \xED\xBF\xBF test");
/*
* Right after the surrogate range.
*/
valid("a 3-bytes encoded UTF-8 character \xEE\x80\x80 right after the "
"surrogate range");
}
ISC_RUN_TEST_IMPL(test_utf8_fourbytes) {
/*
* 0x010000 (first valid unicode character holding in 4 bytes with
* UTF-8) is 0000 00|01 0000 |0000 00|00 0000 which encodes in
*
* 1111 0___ 10__ ____ 10__ ____ 10__ ____
* 1111 0000 1001 0000 1000 0000 1000 0000
*
* So, 0xF0908080.
*
* 0x10FFFF (last valid unicode character holding in 4 bytes with UTF-8
* is 0001 0000 1111 |1111 11|11 1111 which encodes in
* 1111 0___ 10__ ____ 10__ ____ 10__ ____
* 1111 0100 1000 1111 1011 1111 1011 1111
*
* So, 0xF48FBFBF.
*/
invalid("invalid 4-bytes sequence: \xF0\x80\x80\x80 (below min)");
valid("valid 4-bytes sequence: \xF0\x90\x80\x80 (min value)");
valid("valid 4-bytes sequence: \xF4\x8F\xBF\xBF (max value)");
invalid("invalid 4-bytes sequence: \xF4\x9F\xBF\xBF(above max)");
invalid("invalid 4-bytes sequence: \xF0\xC1\xC1\xC1 (extra bytes does "
"not start with 10__)");
}
ISC_RUN_TEST_IMPL(test_utf8_bom) {
validbom("\xEF\xBB\xBF");
validbom("\xEF\xBB\xBF ab");
validbom("\xEF\xBB\xBF\xF4\x8F\xBF\xBF");
invalidbom("\xEF\xBB");
invalidbom("\xEF\xBB\xBE");
invalidbom("\xEF\xBB\xBE ab");
invalidbom("\xEF\xBB\xBE\xF4\x8F\xBF\xBF");
}
ISC_TEST_LIST_START
ISC_TEST_ENTRY(test_utf8_ascii)
ISC_TEST_ENTRY(test_utf8_twobytes)
ISC_TEST_ENTRY(test_utf8_threebytes)
ISC_TEST_ENTRY(test_utf8_utf16_surrogate)
ISC_TEST_ENTRY(test_utf8_fourbytes)
ISC_TEST_ENTRY(test_utf8_bom)
ISC_TEST_LIST_END
ISC_TEST_MAIN