Skip to content

normalize_utf32: skip out-of-range codepoints in the compose loop (fixes #288) - #352

Open
AetherAI3 wants to merge 1 commit into
JuliaStrings:masterfrom
AetherAI3:fix/288-normalize-utf32-oob-codepoint
Open

normalize_utf32: skip out-of-range codepoints in the compose loop (fixes #288)#352
AetherAI3 wants to merge 1 commit into
JuliaStrings:masterfrom
AetherAI3:fix/288-normalize-utf32-oob-codepoint

Conversation

@AetherAI3

Copy link
Copy Markdown

Fixes #288.

Small one. utf8proc_normalize_utf32's UTF8PROC_COMPOSE branch reads each buffer[rpos] and passes it straight to unsafe_get_property, guarded only by if (current_char < 0) continue; (that skip is meant for the grapheme-break sentinel written by utf8proc_decompose_char, not for general invalid input).

unsafe_get_property indexes utf8proc_stage1table[uc >> 8], and utf8proc_stage1table is a const utf8proc_uint16_t[4352]. So a UTF-32 codepoint >= 0x110000 in the input buffer produces uc >> 8 >= 4352 and reads past the table.

The public wrapper utf8proc_get_property already handles both bounds with the pattern uc < 0 || uc >= 0x110000 ? utf8proc_properties : unsafe_get_property(uc), and utf8proc_decompose_char uses if (uc < 0 || uc >= 0x110000) return UTF8PROC_ERROR_NOTASSIGNED;. The fix mirrors that same guard in the one call site that was missing it.

Verification

Built master 0075ed7d with clang + -fsanitize=address,undefined.

Minimal reproducer (buffer with one out-of-range codepoint):

utf8proc_int32_t buf[] = { 0x0041, 0x0301, 0x110000 };
utf8proc_normalize_utf32(buf, 3, UTF8PROC_COMPOSE);
  • Baseline (RED): UBSan says utf8proc.c:237:7: runtime error: index 4352 out of bounds for type 'const utf8proc_uint16_t[4352]'. ASan says AddressSanitizer: global-buffer-overflow ... READ of size 2 inside unsafe_get_property, called from utf8proc_normalize_utf32 line 669, 0 bytes after global variable 'utf8proc_stage1table' ... of size 8704. Same failure class as the SEGV in Segmentation fault in utf8proc_normalize_utf32 with invalid options #288.
  • Post-fix (GREEN): same reproducer returns 1, no ASan/UBSan finding. The invalid codepoint is skipped in the compose pass, same treatment the loop already gives the grapheme-break sentinel above it.

Regression under ASan/UBSan (only the tests that don't need downloaded Unicode data — the sandbox this ran in couldn't reach unicode.org):

  • test/iterate — 673 tests passed
  • test/valid — SUCCEEDED
  • test/case — 2942 tests SUCCEEDED
  • test/misc — NFC round-trip SUCCEEDED, API version 17.0.0
  • test/charwidth — 156996 chars SUCCEEDED
  • test/custom — map_custom SUCCEEDED
  • test/printproperty — OK

Hand-checked smoke: compose of c a f e ́ returns c a f é (U+0063 U+0061 U+0066 U+00E9) unchanged. utf8proc_reencode of hello 世界 still produces the correct UTF-8 bytes.

Notes on scope

The reporter mentions utf8proc_reencode may crash the same way. utf8proc_reencode calls utf8proc_normalize_utf32 first, so this fix covers it transitively. utf8proc_encode_char and charbound_encode_char (called by utf8proc_reencode after normalize) already reject uc >= 0x110000 by returning 0.

Other unsafe_get_property callers were reviewed:

  • utf8proc_decompose_char (utf8proc.c:457) — guarded at line 456 (if (uc < 0 || uc >= 0x110000) return UTF8PROC_ERROR_NOTASSIGNED;).
  • utf8proc_decompose_custom canonical-reordering loop (utf8proc.c:609-610) — the buffer contents there come from utf8proc_decompose_char, which only writes validated codepoints, so those unsafe_get_property calls are safe.
  • The second unsafe_get_property(*starter) in the compose loop (utf8proc.c:698) — starter points into a buffer position we just wrote a validated codepoint into, so it inherits the guard added above.

Diff

+3/-2 in one file, one function.

Base

Applied on top of master 0075ed7d0adba45682ee6bf7a83b10f8fd110163.

Credit

Reported by @shuangxiangkan (#288) with an AFL++ reproducer and ASan trace. Their trace pins the crash exactly at utf8proc.c:237 — the finding did the analytical work.

Note on AI assistance

I used an AI assistant to help trace the crash back to the missing bound and to draft this write-up. The code change and every claim above I verified myself against the built binary.

 JuliaStrings#288)

utf8proc_normalize_utf32's UTF8PROC_COMPOSE branch reads each buffer[rpos]
and passes it to unsafe_get_property, guarded only by
if (current_char < 0) continue; (a skip meant for the grapheme-break
sentinel written by utf8proc_decompose_char, not general invalid input).

unsafe_get_property indexes utf8proc_stage1table[uc >> 8], and
utf8proc_stage1table is const utf8proc_uint16_t[4352]. So a UTF-32
codepoint >= 0x110000 in the input buffer produces uc >> 8 >= 4352 and
reads past the table.

Add the missing bound to the existing skip -- the same
uc < 0 || uc >= 0x110000 shape the public wrapper utf8proc_get_property
already uses (utf8proc.c:243) and that utf8proc_decompose_char rejects
with UTF8PROC_ERROR_NOTASSIGNED (utf8proc.c:456). Invalid codepoints are
dropped the same way grapheme-break sentinels already are.

Reproduced under ASan against master 0075ed7 with a 3-codepoint buffer
{ 0x0041, 0x0301, 0x110000 } and UTF8PROC_COMPOSE:
global-buffer-overflow READ of size 2 at unsafe_get_property, 0 bytes
after utf8proc_stage1table (size 8704). Clean after this patch.

Fixes: JuliaStrings#288
Reported-by: shuangxiangkan (github.com/shuangxiangkan)
Signed-off-by: Brandon Barrante <aetherai@aethersystems.net>
@stevengj

stevengj commented Sep 7, 2026

Copy link
Copy Markdown
Member

This seems reasonable to me. (Mostly we haven't used this function directly ourselves, since we only do normalization on UTF-8 text in Julia, so by the time it is decomposed to UTF-32 it is already validated.)

Can you add a test? Just something in test/misc.c should be fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Segmentation fault in utf8proc_normalize_utf32 with invalid options

2 participants