ICU-23404 ucnv_lmb: clamp toULength before LMBCS reassembly memcpy#4003
Open
evilgensec wants to merge 1 commit into
Open
ICU-23404 ucnv_lmb: clamp toULength before LMBCS reassembly memcpy#4003evilgensec wants to merge 1 commit into
evilgensec wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a defensive check in the LMBCS-to-Unicode conversion path to prevent stack-buffer overflow when reassembling a multi-byte sequence from a reused converter state.
Changes:
- Validates
toULengthagainst the LMBCS reassembly buffer size and returnsU_INVALID_STATE_ERRORon invalid state. - Adds detailed in-code rationale explaining the overflow/arithmetic-wrap risk being mitigated.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
LMBCS toUnicode at the top of _LMBCSToUnicodeWithOffsets reassembles a
partial character from `args->converter->toUBytes` into the local
3-byte stack buffer `LMBCS` (`ULMBCS_CHARSIZE_MAX`). It reads
`size_old = args->converter->toULength` and copies that many bytes into
`LMBCS` without first comparing against `sizeof(LMBCS)`.
`toULength` is `int8_t` and elsewhere in ICU is allowed to hold up to
`UCNV_MAX_CHAR_LEN - 1 = 7`. The LMBCS converter itself never sets it
higher than 3, but the field is part of the public-ish `UConverter`
state and survives across `ucnv_toUnicode` calls. If an application
reuses a converter object across encodings without calling
`ucnv_reset()` (technically API-non-compliant but encountered in the
wild), or a non-LMBCS error callback leaves a longer partial sequence
in `toUBytes`, `size_old` reaches the LMBCS code with a value larger
than 3 and:
* `uprv_memcpy(LMBCS, ...toUBytes, size_old)` overflows the 3-byte
stack array;
* `size_new_maybe_1 = sizeof(LMBCS) - size_old` is `size_t`, so the
subtraction wraps to near-SIZE_MAX and the subsequent MIN may pick
a large `size_new`, compounding the overflow on the second memcpy.
Add a defensive `size_old > sizeof(LMBCS)` check that rejects the
input with `U_INVALID_STATE_ERROR`. Mirrors the pattern used in other
ICU converters that validate their inherited partial-sequence state
before consuming it.
* icu4c/source/common/ucnv_lmb.cpp (_LMBCSToUnicodeWithOffsets): clamp
toULength to LMBCS buffer size before reassembly memcpy.
bb2ca30 to
d541c89
Compare
|
Notice: the branch changed across the force-push!
~ Your Friendly Jira-GitHub PR Checker Bot |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LMBCS toUnicode at the top of
_LMBCSToUnicodeWithOffsets(icu4c/source/common/ucnv_lmb.cpp:1281-1290) reassembles a partial character fromargs->converter->toUBytesinto the local 3-byte stack bufferLMBCS(ULMBCS_CHARSIZE_MAX = 3). It readssize_old = args->converter->toULengthand copies that many bytes intoLMBCSwithout first comparing againstsizeof(LMBCS).toULengthisint8_tand elsewhere in ICU is allowed to hold up toUCNV_MAX_CHAR_LEN - 1 = 7. The LMBCS converter itself never sets it higher than 3, but the field is part of the public-ishUConverterstate and survives acrossucnv_toUnicodecalls. If an application reuses a converter object across encodings without callingucnv_reset()(technically API-non-compliant but encountered in the wild), or a non-LMBCS error callback leaves a longer partial sequence intoUBytes,size_oldreaches the LMBCS code with a value larger than 3 and:uprv_memcpy(LMBCS, ...toUBytes, size_old)overflows the 3-byte stack array;size_new_maybe_1 = sizeof(LMBCS) - size_oldissize_t, so the subtraction wraps to near-SIZE_MAX and the subsequent MIN may pick a largesize_new, compounding the overflow on the second memcpy.Fix
Add a defensive
size_old > sizeof(LMBCS)check that rejects the input withU_INVALID_STATE_ERROR. Mirrors the pattern used in other ICU converters that validate their inherited partial-sequence state before consuming it.Diff
Threat model
Pure caller-error invariant violation (API misuse). Defense-in-depth hardening; not a directly exploitable parser bug. The LMBCS converter does not itself produce
toULength > 3, but several common usage patterns can leave that field in a stale state:UConverter*across encodings withoutucnv_reset()between switches.UConverterToUCallbackwrites more than 3 bytes intotoUBytesfor a non-LMBCS partial sequence, then the application changes the converter's algorithm.Without this guard, both patterns smash the 3-byte stack array. With the guard the converter cleanly returns
U_INVALID_STATE_ERROR.Refs
Same defensive pattern is already present in
_LMBCSFromUnicodeWithOffsetsfor the encode side, and other ICU converters apply equivalenttoULengthvalidation at conversion entry (e.g.ucnv_u8.cpp,ucnvmbcs.cpp).