Skip to content

[release/11.0] Fix NLS ordinal casing of supplementary characters to agree with OrdinalIgnoreCase - #134229

Open
github-actions[bot] wants to merge 1 commit into
release/11.0from
backport/pr-134149-to-release/11.0
Open

github-actions[bot] wants to merge 1 commit into
release/11.0from
backport/pr-134149-to-release/11.0

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Backport of #134149 to release/11.0

/cc @tarekgh

Customer Impact

  • Customer reported
  • Found internally

Ordinal casing is a new feature introduced in .NET 11. Customers using the new APIs on Windows with NLS globalization can receive incorrect results for supplementary-plane characters, including scripts such as Deseret, Adlam, and Vithkuqi. Two strings considered distinct by  OrdinalIgnoreCase  can be converted to the same ordinal-cased value, violating the documented canonicalization contract and potentially causing incorrect matching, deduplication, hashing, or key normalization. The issue is difficult for applications to work around and creates behavior inconsistent with ICU-based platforms. The fix is narrowly scoped to NLS mode, introduces no additional API changes, and aligns casing with the existing  CompareStringOrdinal  behavior. The issue found by aspnet. Look at the comment for more info dotnet/aspnetcore#68912 (comment).

#133950

Regression

  • Yes
  • No

[If yes, specify when the regression was introduced. Provide the PR or commit if known.]

Testing

All regression tests are passed, additionally added more tests to cover the issue we are fixing

Risk

Low, this fixing issue in a new feature introduced in net11, additionally the change is scoped to NLS mode which is not the default mode (ICU mode is the default mode).

…nalIgnoreCase (#134149)

## Summary

Under Windows NLS, ordinal casing and ordinal comparison are backed by
two different Windows APIs that disagree for some supplementary scalars:

- `ToUpperOrdinal` / `ToLowerOrdinal` map via `LCMapStringEx`, which
casts the Deseret upper/lower pairs (for example `U+10428` ->
`U+10400`).
- `OrdinalIgnoreCase` comparison uses `CompareStringOrdinal`, which
treats those same supplementary pairs as **not equal**.

Because of this Windows-level incompatibility, the canonicalization
contract was broken in NLS mode: `ToUpperOrdinal(a) ==
ToUpperOrdinal(b)` no longer matched `string.Equals(a, b,
StringComparison.OrdinalIgnoreCase)`. Two strings that
ordinal-ignore-case comparison considered different could be folded to
the same value by ordinal casing (and vice versa).

## Fix

The change is **NLS only** and **adds no casing tables** (no binary size
impact). After the normal casing pass, any supplementary pair that
casing changed is validated against `CompareStringOrdinal`; if the cased
value fell outside its `OrdinalIgnoreCase` class, the original scalar is
restored. This reconciles ordinal casing output with the comparison
oracle without carrying static data.

Applies to the `string`/`Span` `ToUpperOrdinal`/`ToLowerOrdinal` paths
and to `Rune.ToUpperOrdinal`/`Rune.ToLowerOrdinal`.

**ICU mode is not touched at all.**

## Performance (NLS mode only)

Release benchmarks, matched hosts differing only by
`System.Private.CoreLib`, `DOTNET_SYSTEM_GLOBALIZATION_USENLS=1`:

| Scenario | Ratio | Notes |
|---|---|---|
| ASCII | ~1.0 | Fast path unchanged (ratios shown are measurement
noise) |
| BMP (no surrogates) | ~1.09 | One added vectorized surrogate scan |
| Emoji (surrogate-dense) | ~1.24 | Unchanged surrogate runs are
bulk-skipped |
| Deseret (changed supplementary pairs) | higher | Rare correctness
path; one native validation per genuinely changed pair |

The added cost is confined to NLS mode. ASCII keeps its fast path, pure
BMP adds a single vectorized scan, and unchanged surrogate runs are
skipped in bulk so surrogate-dense text does not pay per-pair. The
overhead is acceptable: this behavior ships as part of a new feature in
.NET 11, the common ICU path is unaffected, and the alternative
(embedding casing data) would increase binary size, which we want to
avoid.

## Tests

Added an NLS-specific test covering the full Deseret block
`U+10400..U+1044F` that asserts `String`/`Span`/`Rune` ordinal casing
agree with `OrdinalIgnoreCase` equality (canonicalization and
collision), plus mixed-string and unpaired-high-surrogate cases. Focused
`OrdinalCasingTests`: 91 passed, 0 failed.

Resolves #133950
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-globalization
See info in area-owners.md if you want to be subscribed.

@tarekgh tarekgh added this to the 11.0.0 milestone Sep 18, 2026
@tarekgh tarekgh added the Servicing-consider Issue for next servicing release review label Sep 18, 2026
@tarekgh

tarekgh commented Sep 18, 2026

Copy link
Copy Markdown
Member

CC @vcsjones

@tarekgh
tarekgh requested a review from artl93 September 18, 2026 23:03

@artl93 artl93 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue in newly introduced feature. Approved.

@tarekgh

tarekgh commented Sep 19, 2026

Copy link
Copy Markdown
Member

/ba-g all failures are known tracked failures and not related.

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

Labels

area-System.Globalization Servicing-consider Issue for next servicing release review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants