CWE-135: Incorrect Calculation of Multi-Byte String Length - C
Overview
C has no native concept of "characters" for multi-byte or wide-character encodings - strlen() counts bytes up to the first NUL, and array indexing offsets to raw bytes or wchar_t elements, never to encoded characters. Code that mixes byte-counting functions (strlen, sizeof on a char[]) with multi-byte or wide-character data - UTF-8 text, or Windows WCHAR/TCHAR buffers - miscounts the string. The result is an undersized allocation, a copy truncated in the middle of a multi-byte sequence, or a buffer overflow when a byte count is used where a character count was needed (or vice versa). The recurring instance is Win32 API misuse, where a byte count is passed where MultiByteToWideChar/WideCharToMultiByte expect a count of WCHAR elements.
Common Vulnerable Patterns
Allocating a UTF-8 buffer from a byte length used as a character count
// VULNERABLE - strlen() returns bytes, not characters
const char *utf8_input = get_user_input(); // e.g. UTF-8 "Hello\xE4\xB8\x96\xE7\x95\x8C" ("Hello世界")
size_t char_count = strlen(utf8_input); // returns 11 (bytes), caller treats it as 11 "characters"
if (char_count <= 10) {
// never taken for this input: 11 > 10, so a limit meant to allow
// 10 characters rejects a 7-character string
save_to_database(utf8_input);
}
char truncated[8];
memcpy(truncated, utf8_input, 7); // byte 7 is the middle of 世 (E4 B8 96)
truncated[7] = '\0'; // "Hello\xE4\xB8" - a half sequence, not valid UTF-8
Why this is vulnerable: strlen() walks bytes, not encoded characters - a multi-byte UTF-8 sequence like 世 (3 bytes) counts as 3 toward the "length" even though it is one character. Any length check, allocation, or truncation built on that count operates on the wrong unit, and truncating on a raw byte boundary can split a multi-byte sequence, producing invalid UTF-8 that later decoders may reject or misinterpret.
Note which way the check fails, because the intuitive reading is backwards. In UTF-8 the byte count is never smaller than the character count, so a byte count tested against a character limit is always stricter than intended, never looser - this input is rejected, not accepted. That is why the bug survives: it arrives as user complaints that non-ASCII names are "too long" rather than as a security finding. The direction that overruns a buffer is the mirror image - a character count used to size something measured in bytes - and a codebase that switches this call site to mb-style counting to stop the false rejections has usually created it one step downstream.
Mixing byte and wide-character counts in a Win32 conversion
// VULNERABLE - buffer sized in bytes, MultiByteToWideChar expects a WCHAR count
char narrow[256];
WCHAR wide[256];
// ...populate narrow with a UTF-8 or ANSI string...
int written = MultiByteToWideChar(CP_UTF8, 0, narrow, -1, wide, sizeof(wide));
// sizeof(wide) is 512 (bytes), but cchWideChar expects a WCHAR *element* count (256)
// the function believes the destination is twice as large as it really is
Why this is vulnerable: MultiByteToWideChar's cchWideChar parameter is a count of WCHAR elements, not bytes. Passing sizeof(wide) (a byte count on a wchar_t[256] array, i.e. 512 on Windows where wchar_t is 2 bytes) tells the API the buffer is twice its real size, so the conversion can write past the end of wide.
Secure Patterns
Track byte length and character count separately
#include <stdlib.h>
#include <string.h>
size_t byte_len = strlen(utf8_input); // bytes - use for allocation/copy sizing
size_t char_len = utf8_char_count(utf8_input); // characters - use for user-facing limits
// (a real UTF-8 decoder: a U8_NEXT loop over
// the bytes, or ICU's u_countChar32 after
// converting to UTF-16 - see below)
if (char_len > 100) {
reject_input();
}
char *buffer = malloc(byte_len + 1); // allocate by BYTE length, not char_len
if (buffer) {
memcpy(buffer, utf8_input, byte_len);
buffer[byte_len] = '\0';
}
Why this works: Allocation and memcpy sizing use the byte length, since that is what actually occupies memory; user-facing limits (message length, display width) use a real character count produced by decoding the UTF-8 sequence rather than assuming one byte equals one character. Keeping the two values distinct and never substituting one for the other removes the miscalculation at its source.
Match the count unit to what the Win32 API expects
WCHAR wide[256];
int written = MultiByteToWideChar(
CP_UTF8, 0,
narrow, -1,
wide,
sizeof(wide) / sizeof(wide[0]) // element count, not byte count
);
if (written == 0) {
// conversion failed or truncated - handle the error, do not use `wide` uninitialized
}
Why this works: sizeof(wide) / sizeof(wide[0]) computes the number of WCHAR elements the buffer holds, matching what cchWideChar expects. The API can now correctly detect when the destination is too small and return an error instead of writing past the buffer's real end.
Use a Unicode library for character-accurate operations
#include <unicode/ustring.h>
#include <unicode/utf8.h>
UErrorCode status = U_ZERO_ERROR;
int32_t utf16_len = 0;
u_strFromUTF8(NULL, 0, &utf16_len, utf8_input, -1, &status); // pre-flight: get required length
if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) {
return; // invalid UTF-8 sequence - reject rather than guess
}
UChar *utf16 = malloc((utf16_len + 1) * sizeof(UChar));
status = U_ZERO_ERROR;
u_strFromUTF8(utf16, utf16_len + 1, NULL, utf8_input, -1, &status);
if (U_SUCCESS(status)) {
int32_t char_count = u_countChar32(utf16, utf16_len); // true character count
}
free(utf16);
Why this works: ICU validates the UTF-8 sequence before converting it and reports the exact buffer size the conversion needs via a pre-flight call (u_strFromUTF8 with a NULL destination), so the allocation is never guessed from a byte-counting function. u_countChar32 counts Unicode code points rather than UTF-16 code units, so a supplementary-plane character stored as a surrogate pair counts once rather than twice - which is exactly what u_strlen on the same buffer would get wrong.
u_countChar32 takes a UChar *, so the UTF-16 conversion above is a prerequisite, not an optional step. To count without converting, iterate the UTF-8 bytes with ICU's U8_NEXT macro instead.
A code point count is still not a count of what a user would call characters. é written as e plus a combining acute is two code points, and a flag or a skin-toned emoji is several; if the limit exists so that text fits a field or a screen, count grapheme clusters with ubrk_open(UBRK_CHARACTER, ...) rather than code points.
Testing
- Test with multi-byte UTF-8 input (e.g. "世界", emoji) at exactly the length limit, one character over, and one byte over to confirm truncation happens at a character boundary, not mid-sequence.
- Build with AddressSanitizer (
-fsanitize=address) and run the same inputs to catch any resulting out-of-bounds read/write. - On Windows, test
MultiByteToWideChar/WideCharToMultiBytecall sites with strings containing characters outside the Basic Multilingual Plane (surrogate pairs) to confirm the element-count/byte-count arguments are not swapped.
Common Pitfalls
- Switching counting functions without updating every related call: replacing a byte-counting function with a character-counting one (or vice versa) at the point where the bug was found, without checking every other allocation/copy call downstream that consumes the same length value - the mismatch just moves to whichever call site still uses the old unit.
- Passing
sizeof(buffer)as a Win32 element count:sizeof()on aWCHAR/wchar_tarray returns a byte count, but functions likeMultiByteToWideCharexpectcchWideCharin elements - dividing bysizeof(buffer[0])is required whenever the buffer isn't a plainchar[], not optional. - Treating
-1forcbMultiByte/length arguments as always safe: passing-1tells the API the input is NUL-terminated and to include the terminator in its count; if the source buffer isn't actually NUL-terminated (e.g. raw bytes from a fixed-size network read), the function reads past the intended data.