Fix incorrect early exit in SortKey.Compare and seal type - #31779

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey
Feb 6, 2020
Merged

Fix incorrect early exit in SortKey.Compare and seal type#31779
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

The method SortKey.Compare contains an early incorrect exit which can cause it to return 0 if all of the below conditions hold:

  • The application is using the invariant globalization mode, and
  • Neither sortKey1 nor sortKey2 represents the empty string, and
  • sortKey1 is a prefix of sortKey2 (or vice versa).

In a nutshell, this means that under the invariant globalization mode, the expression compareInfo.GetSortKey("he").Equals(compareInfo.GetSortKey("hello!")) incorrectly returns true.

There may also be some inputs under the non-invariant mode which trigger this incorrect output, but I didn't attempt this since it would be highly dependent on how the active version of NLS or ICU produces sort keys. It's far easier to trigger this bug under the invariant mode.

This PR addresses that issue in SortKey.Compare. Additionally, there are some performance optimizations made to SortKey.Equals and SortKey.GetHashCode. This also builds on #31761 by sealing the SortKey type and devirtualizing the methods.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

There's another interesting behavior here. Since under the invariant mode sort keys are a raw projection of chars to bytes, there are endianness issues to consider.

For example, under the invariant mode:

/* on a little-endian machine */// "ĕ" = "\u0115"string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns > 0/* on a big-endian machine */string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns < 0

Open question: Is this discrepancy allowable? I don't know if there's a valid scenario in which somebody uses GetSortKey under the invariant mode. But if there is, it might be necessary to make CompareInfo.Compare and SortKey.Compare agree on the result.

@GrabYourPitchforksGrabYourPitchforks added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

I wouldn't care much if someone using InvarantMode and Sort Keys. sort keys are really useful for scenarios that use real cultures and cares about linguistic behavior. The invariant mode is not for such scenarios at all.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Would it make sense for us to block the GetSortKey API under the invariant mode?

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Globalization/SortKey.cs Outdated
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Another possible solution (if we do care about CompareInfo and SortKey not getting out of sync on the same box) is to have logic akin to this in SortKey.Compare:

if(GlobalizationMode.Invariant){returnkey1._bytes.AsChars().SequenceCompareTo(key2._bytes.AsChars());}else{returnkey1._bytes.SequenceCompareTo(key2._bytes);}

@jkotas

Copy link
Copy Markdown
Member

there are endianness issues to consider.

Would it make sense to swap the bytes when creating the sort key in InvariantMode to fix this?

GlobalizationMode.Invariant mode as it exists today is not very useful unless you are building a microservices that does not ever use any globalization APIs. I do not think we need to worry about what SortKeys do in it too much. I am wondering whether we can make GlobalizationMode.Invariant to be more useful by making it to be roughly on par with the globalization stack that came with classic Mono. The data for it were still small, but it was actually reasonably useful. Some of this overlaps with your idea to include Unicode data with the runtime.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I do not think we need to worry about what SortKeys do in it too much.

Sounds like that's two votes for "don't bother with endianness concerns"? :)

@jkotas

Copy link
Copy Markdown
Member

That, or if you would like to write the few lines of code to swap endianess in the invariantmode sort key generation - that's fine too.

@pentp

pentp commented Feb 5, 2020

Copy link
Copy Markdown
Contributor

Making GlobalizationMode.Invariant more useful/practical sounds like a good idea. I've tried it on a (micro)service that should only use InvariantCulture (or en-US), but changing to GlobalizationMode.Invariant is still a bit scary because of its quirks and it's probably not a tested scenario for Orleans or AspNetCore or Newtonsoft.Json...

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Rename internal method InvariantToUpper to InvariantCaseFold so that it can be easily updated in the future
  • Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags
  • Simplify SortKey.Compare per PR feedback
  • Update InvariantCreateSortKey to be endianness-aware and use safer span-based logic in lieu of pointers

@GrabYourPitchforksGrabYourPitchforks removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags

GetHashCode doesn't allow CompareOptions.StringSort I think.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 5, 2020

Copy link
Copy Markdown
MemberAuthor

GetHashCode doesn't allow CompareOptions.StringSort I think.

It has to. GetHashCode is internally built on top of GetSortKey, which means that it has to allow the same set of parameters.

This same logic applies to Compare. In practice, Compare can be thought of as a glorified "get sort key for A" and "get sort key for B", then perform a byte-wise comparison of the two sort keys.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Suppress one test case on Win7, as LCMapStringEx and CompareStringEx return different results and it's throwing off the unit test. This is only cropping up now because I modified the SortKey.Compare unit test to also ensure that CompareInfo.Compare returns the same result.

@GrabYourPitchforks
GrabYourPitchforks merged commit 6766eb7 into dotnet:masterFeb 6, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the sortkey branch February 6, 2020 04:49
@ghostghost added the will_lock_this label Dec 6, 2020
@ghostghost locked as resolved and limited conversation to collaborators Jan 5, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@tarekgh@jkotas@pentp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix incorrect early exit in SortKey.Compare and seal type - #31779

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey
Feb 6, 2020
Merged

Fix incorrect early exit in SortKey.Compare and seal type#31779
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

The method SortKey.Compare contains an early incorrect exit which can cause it to return 0 if all of the below conditions hold:

  • The application is using the invariant globalization mode, and
  • Neither sortKey1 nor sortKey2 represents the empty string, and
  • sortKey1 is a prefix of sortKey2 (or vice versa).

In a nutshell, this means that under the invariant globalization mode, the expression compareInfo.GetSortKey("he").Equals(compareInfo.GetSortKey("hello!")) incorrectly returns true.

There may also be some inputs under the non-invariant mode which trigger this incorrect output, but I didn't attempt this since it would be highly dependent on how the active version of NLS or ICU produces sort keys. It's far easier to trigger this bug under the invariant mode.

This PR addresses that issue in SortKey.Compare. Additionally, there are some performance optimizations made to SortKey.Equals and SortKey.GetHashCode. This also builds on #31761 by sealing the SortKey type and devirtualizing the methods.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

There's another interesting behavior here. Since under the invariant mode sort keys are a raw projection of chars to bytes, there are endianness issues to consider.

For example, under the invariant mode:

/* on a little-endian machine */// "ĕ" = "\u0115"string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns > 0/* on a big-endian machine */string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns < 0

Open question: Is this discrepancy allowable? I don't know if there's a valid scenario in which somebody uses GetSortKey under the invariant mode. But if there is, it might be necessary to make CompareInfo.Compare and SortKey.Compare agree on the result.

@GrabYourPitchforksGrabYourPitchforks added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

I wouldn't care much if someone using InvarantMode and Sort Keys. sort keys are really useful for scenarios that use real cultures and cares about linguistic behavior. The invariant mode is not for such scenarios at all.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Would it make sense for us to block the GetSortKey API under the invariant mode?

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Globalization/SortKey.cs Outdated
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Another possible solution (if we do care about CompareInfo and SortKey not getting out of sync on the same box) is to have logic akin to this in SortKey.Compare:

if(GlobalizationMode.Invariant){returnkey1._bytes.AsChars().SequenceCompareTo(key2._bytes.AsChars());}else{returnkey1._bytes.SequenceCompareTo(key2._bytes);}

@jkotas

Copy link
Copy Markdown
Member

there are endianness issues to consider.

Would it make sense to swap the bytes when creating the sort key in InvariantMode to fix this?

GlobalizationMode.Invariant mode as it exists today is not very useful unless you are building a microservices that does not ever use any globalization APIs. I do not think we need to worry about what SortKeys do in it too much. I am wondering whether we can make GlobalizationMode.Invariant to be more useful by making it to be roughly on par with the globalization stack that came with classic Mono. The data for it were still small, but it was actually reasonably useful. Some of this overlaps with your idea to include Unicode data with the runtime.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I do not think we need to worry about what SortKeys do in it too much.

Sounds like that's two votes for "don't bother with endianness concerns"? :)

@jkotas

Copy link
Copy Markdown
Member

That, or if you would like to write the few lines of code to swap endianess in the invariantmode sort key generation - that's fine too.

@pentp

pentp commented Feb 5, 2020

Copy link
Copy Markdown
Contributor

Making GlobalizationMode.Invariant more useful/practical sounds like a good idea. I've tried it on a (micro)service that should only use InvariantCulture (or en-US), but changing to GlobalizationMode.Invariant is still a bit scary because of its quirks and it's probably not a tested scenario for Orleans or AspNetCore or Newtonsoft.Json...

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Rename internal method InvariantToUpper to InvariantCaseFold so that it can be easily updated in the future
  • Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags
  • Simplify SortKey.Compare per PR feedback
  • Update InvariantCreateSortKey to be endianness-aware and use safer span-based logic in lieu of pointers

@GrabYourPitchforksGrabYourPitchforks removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags

GetHashCode doesn't allow CompareOptions.StringSort I think.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 5, 2020

Copy link
Copy Markdown
MemberAuthor

GetHashCode doesn't allow CompareOptions.StringSort I think.

It has to. GetHashCode is internally built on top of GetSortKey, which means that it has to allow the same set of parameters.

This same logic applies to Compare. In practice, Compare can be thought of as a glorified "get sort key for A" and "get sort key for B", then perform a byte-wise comparison of the two sort keys.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Suppress one test case on Win7, as LCMapStringEx and CompareStringEx return different results and it's throwing off the unit test. This is only cropping up now because I modified the SortKey.Compare unit test to also ensure that CompareInfo.Compare returns the same result.

@GrabYourPitchforks
GrabYourPitchforks merged commit 6766eb7 into dotnet:masterFeb 6, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the sortkey branch February 6, 2020 04:49
@ghostghost added the will_lock_this label Dec 6, 2020
@ghostghost locked as resolved and limited conversation to collaborators Jan 5, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@tarekgh@jkotas@pentp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix incorrect early exit in SortKey.Compare and seal type - #31779

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey
Feb 6, 2020
Merged

Fix incorrect early exit in SortKey.Compare and seal type#31779
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

The method SortKey.Compare contains an early incorrect exit which can cause it to return 0 if all of the below conditions hold:

  • The application is using the invariant globalization mode, and
  • Neither sortKey1 nor sortKey2 represents the empty string, and
  • sortKey1 is a prefix of sortKey2 (or vice versa).

In a nutshell, this means that under the invariant globalization mode, the expression compareInfo.GetSortKey("he").Equals(compareInfo.GetSortKey("hello!")) incorrectly returns true.

There may also be some inputs under the non-invariant mode which trigger this incorrect output, but I didn't attempt this since it would be highly dependent on how the active version of NLS or ICU produces sort keys. It's far easier to trigger this bug under the invariant mode.

This PR addresses that issue in SortKey.Compare. Additionally, there are some performance optimizations made to SortKey.Equals and SortKey.GetHashCode. This also builds on #31761 by sealing the SortKey type and devirtualizing the methods.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

There's another interesting behavior here. Since under the invariant mode sort keys are a raw projection of chars to bytes, there are endianness issues to consider.

For example, under the invariant mode:

/* on a little-endian machine */// "ĕ" = "\u0115"string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns > 0/* on a big-endian machine */string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns < 0

Open question: Is this discrepancy allowable? I don't know if there's a valid scenario in which somebody uses GetSortKey under the invariant mode. But if there is, it might be necessary to make CompareInfo.Compare and SortKey.Compare agree on the result.

@GrabYourPitchforksGrabYourPitchforks added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

I wouldn't care much if someone using InvarantMode and Sort Keys. sort keys are really useful for scenarios that use real cultures and cares about linguistic behavior. The invariant mode is not for such scenarios at all.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Would it make sense for us to block the GetSortKey API under the invariant mode?

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Globalization/SortKey.cs Outdated
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Another possible solution (if we do care about CompareInfo and SortKey not getting out of sync on the same box) is to have logic akin to this in SortKey.Compare:

if(GlobalizationMode.Invariant){returnkey1._bytes.AsChars().SequenceCompareTo(key2._bytes.AsChars());}else{returnkey1._bytes.SequenceCompareTo(key2._bytes);}

@jkotas

Copy link
Copy Markdown
Member

there are endianness issues to consider.

Would it make sense to swap the bytes when creating the sort key in InvariantMode to fix this?

GlobalizationMode.Invariant mode as it exists today is not very useful unless you are building a microservices that does not ever use any globalization APIs. I do not think we need to worry about what SortKeys do in it too much. I am wondering whether we can make GlobalizationMode.Invariant to be more useful by making it to be roughly on par with the globalization stack that came with classic Mono. The data for it were still small, but it was actually reasonably useful. Some of this overlaps with your idea to include Unicode data with the runtime.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I do not think we need to worry about what SortKeys do in it too much.

Sounds like that's two votes for "don't bother with endianness concerns"? :)

@jkotas

Copy link
Copy Markdown
Member

That, or if you would like to write the few lines of code to swap endianess in the invariantmode sort key generation - that's fine too.

@pentp

pentp commented Feb 5, 2020

Copy link
Copy Markdown
Contributor

Making GlobalizationMode.Invariant more useful/practical sounds like a good idea. I've tried it on a (micro)service that should only use InvariantCulture (or en-US), but changing to GlobalizationMode.Invariant is still a bit scary because of its quirks and it's probably not a tested scenario for Orleans or AspNetCore or Newtonsoft.Json...

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Rename internal method InvariantToUpper to InvariantCaseFold so that it can be easily updated in the future
  • Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags
  • Simplify SortKey.Compare per PR feedback
  • Update InvariantCreateSortKey to be endianness-aware and use safer span-based logic in lieu of pointers

@GrabYourPitchforksGrabYourPitchforks removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags

GetHashCode doesn't allow CompareOptions.StringSort I think.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 5, 2020

Copy link
Copy Markdown
MemberAuthor

GetHashCode doesn't allow CompareOptions.StringSort I think.

It has to. GetHashCode is internally built on top of GetSortKey, which means that it has to allow the same set of parameters.

This same logic applies to Compare. In practice, Compare can be thought of as a glorified "get sort key for A" and "get sort key for B", then perform a byte-wise comparison of the two sort keys.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Suppress one test case on Win7, as LCMapStringEx and CompareStringEx return different results and it's throwing off the unit test. This is only cropping up now because I modified the SortKey.Compare unit test to also ensure that CompareInfo.Compare returns the same result.

@GrabYourPitchforks
GrabYourPitchforks merged commit 6766eb7 into dotnet:masterFeb 6, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the sortkey branch February 6, 2020 04:49
@ghostghost added the will_lock_this label Dec 6, 2020
@ghostghost locked as resolved and limited conversation to collaborators Jan 5, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@tarekgh@jkotas@pentp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix incorrect early exit in SortKey.Compare and seal type - #31779

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey
Feb 6, 2020
Merged

Fix incorrect early exit in SortKey.Compare and seal type#31779
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

The method SortKey.Compare contains an early incorrect exit which can cause it to return 0 if all of the below conditions hold:

  • The application is using the invariant globalization mode, and
  • Neither sortKey1 nor sortKey2 represents the empty string, and
  • sortKey1 is a prefix of sortKey2 (or vice versa).

In a nutshell, this means that under the invariant globalization mode, the expression compareInfo.GetSortKey("he").Equals(compareInfo.GetSortKey("hello!")) incorrectly returns true.

There may also be some inputs under the non-invariant mode which trigger this incorrect output, but I didn't attempt this since it would be highly dependent on how the active version of NLS or ICU produces sort keys. It's far easier to trigger this bug under the invariant mode.

This PR addresses that issue in SortKey.Compare. Additionally, there are some performance optimizations made to SortKey.Equals and SortKey.GetHashCode. This also builds on #31761 by sealing the SortKey type and devirtualizing the methods.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

There's another interesting behavior here. Since under the invariant mode sort keys are a raw projection of chars to bytes, there are endianness issues to consider.

For example, under the invariant mode:

/* on a little-endian machine */// "ĕ" = "\u0115"string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns > 0/* on a big-endian machine */string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns < 0

Open question: Is this discrepancy allowable? I don't know if there's a valid scenario in which somebody uses GetSortKey under the invariant mode. But if there is, it might be necessary to make CompareInfo.Compare and SortKey.Compare agree on the result.

@GrabYourPitchforksGrabYourPitchforks added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

I wouldn't care much if someone using InvarantMode and Sort Keys. sort keys are really useful for scenarios that use real cultures and cares about linguistic behavior. The invariant mode is not for such scenarios at all.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Would it make sense for us to block the GetSortKey API under the invariant mode?

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Globalization/SortKey.cs Outdated
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Another possible solution (if we do care about CompareInfo and SortKey not getting out of sync on the same box) is to have logic akin to this in SortKey.Compare:

if(GlobalizationMode.Invariant){returnkey1._bytes.AsChars().SequenceCompareTo(key2._bytes.AsChars());}else{returnkey1._bytes.SequenceCompareTo(key2._bytes);}

@jkotas

Copy link
Copy Markdown
Member

there are endianness issues to consider.

Would it make sense to swap the bytes when creating the sort key in InvariantMode to fix this?

GlobalizationMode.Invariant mode as it exists today is not very useful unless you are building a microservices that does not ever use any globalization APIs. I do not think we need to worry about what SortKeys do in it too much. I am wondering whether we can make GlobalizationMode.Invariant to be more useful by making it to be roughly on par with the globalization stack that came with classic Mono. The data for it were still small, but it was actually reasonably useful. Some of this overlaps with your idea to include Unicode data with the runtime.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I do not think we need to worry about what SortKeys do in it too much.

Sounds like that's two votes for "don't bother with endianness concerns"? :)

@jkotas

Copy link
Copy Markdown
Member

That, or if you would like to write the few lines of code to swap endianess in the invariantmode sort key generation - that's fine too.

@pentp

pentp commented Feb 5, 2020

Copy link
Copy Markdown
Contributor

Making GlobalizationMode.Invariant more useful/practical sounds like a good idea. I've tried it on a (micro)service that should only use InvariantCulture (or en-US), but changing to GlobalizationMode.Invariant is still a bit scary because of its quirks and it's probably not a tested scenario for Orleans or AspNetCore or Newtonsoft.Json...

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Rename internal method InvariantToUpper to InvariantCaseFold so that it can be easily updated in the future
  • Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags
  • Simplify SortKey.Compare per PR feedback
  • Update InvariantCreateSortKey to be endianness-aware and use safer span-based logic in lieu of pointers

@GrabYourPitchforksGrabYourPitchforks removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags

GetHashCode doesn't allow CompareOptions.StringSort I think.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 5, 2020

Copy link
Copy Markdown
MemberAuthor

GetHashCode doesn't allow CompareOptions.StringSort I think.

It has to. GetHashCode is internally built on top of GetSortKey, which means that it has to allow the same set of parameters.

This same logic applies to Compare. In practice, Compare can be thought of as a glorified "get sort key for A" and "get sort key for B", then perform a byte-wise comparison of the two sort keys.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Suppress one test case on Win7, as LCMapStringEx and CompareStringEx return different results and it's throwing off the unit test. This is only cropping up now because I modified the SortKey.Compare unit test to also ensure that CompareInfo.Compare returns the same result.

@GrabYourPitchforks
GrabYourPitchforks merged commit 6766eb7 into dotnet:masterFeb 6, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the sortkey branch February 6, 2020 04:49
@ghostghost added the will_lock_this label Dec 6, 2020
@ghostghost locked as resolved and limited conversation to collaborators Jan 5, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@tarekgh@jkotas@pentp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix incorrect early exit in SortKey.Compare and seal type - #31779

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey
Feb 6, 2020
Merged

Fix incorrect early exit in SortKey.Compare and seal type#31779
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

The method SortKey.Compare contains an early incorrect exit which can cause it to return 0 if all of the below conditions hold:

  • The application is using the invariant globalization mode, and
  • Neither sortKey1 nor sortKey2 represents the empty string, and
  • sortKey1 is a prefix of sortKey2 (or vice versa).

In a nutshell, this means that under the invariant globalization mode, the expression compareInfo.GetSortKey("he").Equals(compareInfo.GetSortKey("hello!")) incorrectly returns true.

There may also be some inputs under the non-invariant mode which trigger this incorrect output, but I didn't attempt this since it would be highly dependent on how the active version of NLS or ICU produces sort keys. It's far easier to trigger this bug under the invariant mode.

This PR addresses that issue in SortKey.Compare. Additionally, there are some performance optimizations made to SortKey.Equals and SortKey.GetHashCode. This also builds on #31761 by sealing the SortKey type and devirtualizing the methods.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

There's another interesting behavior here. Since under the invariant mode sort keys are a raw projection of chars to bytes, there are endianness issues to consider.

For example, under the invariant mode:

/* on a little-endian machine */// "ĕ" = "\u0115"string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns > 0/* on a big-endian machine */string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns < 0

Open question: Is this discrepancy allowable? I don't know if there's a valid scenario in which somebody uses GetSortKey under the invariant mode. But if there is, it might be necessary to make CompareInfo.Compare and SortKey.Compare agree on the result.

@GrabYourPitchforksGrabYourPitchforks added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

I wouldn't care much if someone using InvarantMode and Sort Keys. sort keys are really useful for scenarios that use real cultures and cares about linguistic behavior. The invariant mode is not for such scenarios at all.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Would it make sense for us to block the GetSortKey API under the invariant mode?

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Globalization/SortKey.cs Outdated
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Another possible solution (if we do care about CompareInfo and SortKey not getting out of sync on the same box) is to have logic akin to this in SortKey.Compare:

if(GlobalizationMode.Invariant){returnkey1._bytes.AsChars().SequenceCompareTo(key2._bytes.AsChars());}else{returnkey1._bytes.SequenceCompareTo(key2._bytes);}

@jkotas

Copy link
Copy Markdown
Member

there are endianness issues to consider.

Would it make sense to swap the bytes when creating the sort key in InvariantMode to fix this?

GlobalizationMode.Invariant mode as it exists today is not very useful unless you are building a microservices that does not ever use any globalization APIs. I do not think we need to worry about what SortKeys do in it too much. I am wondering whether we can make GlobalizationMode.Invariant to be more useful by making it to be roughly on par with the globalization stack that came with classic Mono. The data for it were still small, but it was actually reasonably useful. Some of this overlaps with your idea to include Unicode data with the runtime.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I do not think we need to worry about what SortKeys do in it too much.

Sounds like that's two votes for "don't bother with endianness concerns"? :)

@jkotas

Copy link
Copy Markdown
Member

That, or if you would like to write the few lines of code to swap endianess in the invariantmode sort key generation - that's fine too.

@pentp

pentp commented Feb 5, 2020

Copy link
Copy Markdown
Contributor

Making GlobalizationMode.Invariant more useful/practical sounds like a good idea. I've tried it on a (micro)service that should only use InvariantCulture (or en-US), but changing to GlobalizationMode.Invariant is still a bit scary because of its quirks and it's probably not a tested scenario for Orleans or AspNetCore or Newtonsoft.Json...

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Rename internal method InvariantToUpper to InvariantCaseFold so that it can be easily updated in the future
  • Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags
  • Simplify SortKey.Compare per PR feedback
  • Update InvariantCreateSortKey to be endianness-aware and use safer span-based logic in lieu of pointers

@GrabYourPitchforksGrabYourPitchforks removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags

GetHashCode doesn't allow CompareOptions.StringSort I think.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 5, 2020

Copy link
Copy Markdown
MemberAuthor

GetHashCode doesn't allow CompareOptions.StringSort I think.

It has to. GetHashCode is internally built on top of GetSortKey, which means that it has to allow the same set of parameters.

This same logic applies to Compare. In practice, Compare can be thought of as a glorified "get sort key for A" and "get sort key for B", then perform a byte-wise comparison of the two sort keys.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Suppress one test case on Win7, as LCMapStringEx and CompareStringEx return different results and it's throwing off the unit test. This is only cropping up now because I modified the SortKey.Compare unit test to also ensure that CompareInfo.Compare returns the same result.

@GrabYourPitchforks
GrabYourPitchforks merged commit 6766eb7 into dotnet:masterFeb 6, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the sortkey branch February 6, 2020 04:49
@ghostghost added the will_lock_this label Dec 6, 2020
@ghostghost locked as resolved and limited conversation to collaborators Jan 5, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@tarekgh@jkotas@pentp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix incorrect early exit in SortKey.Compare and seal type - #31779

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey
Feb 6, 2020
Merged

Fix incorrect early exit in SortKey.Compare and seal type#31779
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

The method SortKey.Compare contains an early incorrect exit which can cause it to return 0 if all of the below conditions hold:

  • The application is using the invariant globalization mode, and
  • Neither sortKey1 nor sortKey2 represents the empty string, and
  • sortKey1 is a prefix of sortKey2 (or vice versa).

In a nutshell, this means that under the invariant globalization mode, the expression compareInfo.GetSortKey("he").Equals(compareInfo.GetSortKey("hello!")) incorrectly returns true.

There may also be some inputs under the non-invariant mode which trigger this incorrect output, but I didn't attempt this since it would be highly dependent on how the active version of NLS or ICU produces sort keys. It's far easier to trigger this bug under the invariant mode.

This PR addresses that issue in SortKey.Compare. Additionally, there are some performance optimizations made to SortKey.Equals and SortKey.GetHashCode. This also builds on #31761 by sealing the SortKey type and devirtualizing the methods.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

There's another interesting behavior here. Since under the invariant mode sort keys are a raw projection of chars to bytes, there are endianness issues to consider.

For example, under the invariant mode:

/* on a little-endian machine */// "ĕ" = "\u0115"string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns > 0/* on a big-endian machine */string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns < 0

Open question: Is this discrepancy allowable? I don't know if there's a valid scenario in which somebody uses GetSortKey under the invariant mode. But if there is, it might be necessary to make CompareInfo.Compare and SortKey.Compare agree on the result.

@GrabYourPitchforksGrabYourPitchforks added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

I wouldn't care much if someone using InvarantMode and Sort Keys. sort keys are really useful for scenarios that use real cultures and cares about linguistic behavior. The invariant mode is not for such scenarios at all.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Would it make sense for us to block the GetSortKey API under the invariant mode?

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Globalization/SortKey.cs Outdated
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Another possible solution (if we do care about CompareInfo and SortKey not getting out of sync on the same box) is to have logic akin to this in SortKey.Compare:

if(GlobalizationMode.Invariant){returnkey1._bytes.AsChars().SequenceCompareTo(key2._bytes.AsChars());}else{returnkey1._bytes.SequenceCompareTo(key2._bytes);}

@jkotas

Copy link
Copy Markdown
Member

there are endianness issues to consider.

Would it make sense to swap the bytes when creating the sort key in InvariantMode to fix this?

GlobalizationMode.Invariant mode as it exists today is not very useful unless you are building a microservices that does not ever use any globalization APIs. I do not think we need to worry about what SortKeys do in it too much. I am wondering whether we can make GlobalizationMode.Invariant to be more useful by making it to be roughly on par with the globalization stack that came with classic Mono. The data for it were still small, but it was actually reasonably useful. Some of this overlaps with your idea to include Unicode data with the runtime.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I do not think we need to worry about what SortKeys do in it too much.

Sounds like that's two votes for "don't bother with endianness concerns"? :)

@jkotas

Copy link
Copy Markdown
Member

That, or if you would like to write the few lines of code to swap endianess in the invariantmode sort key generation - that's fine too.

@pentp

pentp commented Feb 5, 2020

Copy link
Copy Markdown
Contributor

Making GlobalizationMode.Invariant more useful/practical sounds like a good idea. I've tried it on a (micro)service that should only use InvariantCulture (or en-US), but changing to GlobalizationMode.Invariant is still a bit scary because of its quirks and it's probably not a tested scenario for Orleans or AspNetCore or Newtonsoft.Json...

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Rename internal method InvariantToUpper to InvariantCaseFold so that it can be easily updated in the future
  • Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags
  • Simplify SortKey.Compare per PR feedback
  • Update InvariantCreateSortKey to be endianness-aware and use safer span-based logic in lieu of pointers

@GrabYourPitchforksGrabYourPitchforks removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags

GetHashCode doesn't allow CompareOptions.StringSort I think.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 5, 2020

Copy link
Copy Markdown
MemberAuthor

GetHashCode doesn't allow CompareOptions.StringSort I think.

It has to. GetHashCode is internally built on top of GetSortKey, which means that it has to allow the same set of parameters.

This same logic applies to Compare. In practice, Compare can be thought of as a glorified "get sort key for A" and "get sort key for B", then perform a byte-wise comparison of the two sort keys.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Suppress one test case on Win7, as LCMapStringEx and CompareStringEx return different results and it's throwing off the unit test. This is only cropping up now because I modified the SortKey.Compare unit test to also ensure that CompareInfo.Compare returns the same result.

@GrabYourPitchforks
GrabYourPitchforks merged commit 6766eb7 into dotnet:masterFeb 6, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the sortkey branch February 6, 2020 04:49
@ghostghost added the will_lock_this label Dec 6, 2020
@ghostghost locked as resolved and limited conversation to collaborators Jan 5, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@tarekgh@jkotas@pentp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix incorrect early exit in SortKey.Compare and seal type - #31779

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey
Feb 6, 2020
Merged

Fix incorrect early exit in SortKey.Compare and seal type#31779
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

The method SortKey.Compare contains an early incorrect exit which can cause it to return 0 if all of the below conditions hold:

  • The application is using the invariant globalization mode, and
  • Neither sortKey1 nor sortKey2 represents the empty string, and
  • sortKey1 is a prefix of sortKey2 (or vice versa).

In a nutshell, this means that under the invariant globalization mode, the expression compareInfo.GetSortKey("he").Equals(compareInfo.GetSortKey("hello!")) incorrectly returns true.

There may also be some inputs under the non-invariant mode which trigger this incorrect output, but I didn't attempt this since it would be highly dependent on how the active version of NLS or ICU produces sort keys. It's far easier to trigger this bug under the invariant mode.

This PR addresses that issue in SortKey.Compare. Additionally, there are some performance optimizations made to SortKey.Equals and SortKey.GetHashCode. This also builds on #31761 by sealing the SortKey type and devirtualizing the methods.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

There's another interesting behavior here. Since under the invariant mode sort keys are a raw projection of chars to bytes, there are endianness issues to consider.

For example, under the invariant mode:

/* on a little-endian machine */// "ĕ" = "\u0115"string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns > 0/* on a big-endian machine */string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns < 0

Open question: Is this discrepancy allowable? I don't know if there's a valid scenario in which somebody uses GetSortKey under the invariant mode. But if there is, it might be necessary to make CompareInfo.Compare and SortKey.Compare agree on the result.

@GrabYourPitchforksGrabYourPitchforks added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

I wouldn't care much if someone using InvarantMode and Sort Keys. sort keys are really useful for scenarios that use real cultures and cares about linguistic behavior. The invariant mode is not for such scenarios at all.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Would it make sense for us to block the GetSortKey API under the invariant mode?

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Globalization/SortKey.cs Outdated
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Another possible solution (if we do care about CompareInfo and SortKey not getting out of sync on the same box) is to have logic akin to this in SortKey.Compare:

if(GlobalizationMode.Invariant){returnkey1._bytes.AsChars().SequenceCompareTo(key2._bytes.AsChars());}else{returnkey1._bytes.SequenceCompareTo(key2._bytes);}

@jkotas

Copy link
Copy Markdown
Member

there are endianness issues to consider.

Would it make sense to swap the bytes when creating the sort key in InvariantMode to fix this?

GlobalizationMode.Invariant mode as it exists today is not very useful unless you are building a microservices that does not ever use any globalization APIs. I do not think we need to worry about what SortKeys do in it too much. I am wondering whether we can make GlobalizationMode.Invariant to be more useful by making it to be roughly on par with the globalization stack that came with classic Mono. The data for it were still small, but it was actually reasonably useful. Some of this overlaps with your idea to include Unicode data with the runtime.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I do not think we need to worry about what SortKeys do in it too much.

Sounds like that's two votes for "don't bother with endianness concerns"? :)

@jkotas

Copy link
Copy Markdown
Member

That, or if you would like to write the few lines of code to swap endianess in the invariantmode sort key generation - that's fine too.

@pentp

pentp commented Feb 5, 2020

Copy link
Copy Markdown
Contributor

Making GlobalizationMode.Invariant more useful/practical sounds like a good idea. I've tried it on a (micro)service that should only use InvariantCulture (or en-US), but changing to GlobalizationMode.Invariant is still a bit scary because of its quirks and it's probably not a tested scenario for Orleans or AspNetCore or Newtonsoft.Json...

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Rename internal method InvariantToUpper to InvariantCaseFold so that it can be easily updated in the future
  • Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags
  • Simplify SortKey.Compare per PR feedback
  • Update InvariantCreateSortKey to be endianness-aware and use safer span-based logic in lieu of pointers

@GrabYourPitchforksGrabYourPitchforks removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags

GetHashCode doesn't allow CompareOptions.StringSort I think.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 5, 2020

Copy link
Copy Markdown
MemberAuthor

GetHashCode doesn't allow CompareOptions.StringSort I think.

It has to. GetHashCode is internally built on top of GetSortKey, which means that it has to allow the same set of parameters.

This same logic applies to Compare. In practice, Compare can be thought of as a glorified "get sort key for A" and "get sort key for B", then perform a byte-wise comparison of the two sort keys.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Suppress one test case on Win7, as LCMapStringEx and CompareStringEx return different results and it's throwing off the unit test. This is only cropping up now because I modified the SortKey.Compare unit test to also ensure that CompareInfo.Compare returns the same result.

@GrabYourPitchforks
GrabYourPitchforks merged commit 6766eb7 into dotnet:masterFeb 6, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the sortkey branch February 6, 2020 04:49
@ghostghost added the will_lock_this label Dec 6, 2020
@ghostghost locked as resolved and limited conversation to collaborators Jan 5, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@tarekgh@jkotas@pentp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix incorrect early exit in SortKey.Compare and seal type - #31779

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey
Feb 6, 2020
Merged

Fix incorrect early exit in SortKey.Compare and seal type#31779
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
GrabYourPitchforks:sortkey

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

The method SortKey.Compare contains an early incorrect exit which can cause it to return 0 if all of the below conditions hold:

  • The application is using the invariant globalization mode, and
  • Neither sortKey1 nor sortKey2 represents the empty string, and
  • sortKey1 is a prefix of sortKey2 (or vice versa).

In a nutshell, this means that under the invariant globalization mode, the expression compareInfo.GetSortKey("he").Equals(compareInfo.GetSortKey("hello!")) incorrectly returns true.

There may also be some inputs under the non-invariant mode which trigger this incorrect output, but I didn't attempt this since it would be highly dependent on how the active version of NLS or ICU produces sort keys. It's far easier to trigger this bug under the invariant mode.

This PR addresses that issue in SortKey.Compare. Additionally, there are some performance optimizations made to SortKey.Equals and SortKey.GetHashCode. This also builds on #31761 by sealing the SortKey type and devirtualizing the methods.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

There's another interesting behavior here. Since under the invariant mode sort keys are a raw projection of chars to bytes, there are endianness issues to consider.

For example, under the invariant mode:

/* on a little-endian machine */// "ĕ" = "\u0115"string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns > 0/* on a big-endian machine */string.Compare("e","ĕ",StringComparison.Ordinal);// returns < 0CompareInfo.Compare("e","ĕ");// returns < 0SortKey.Compare(CompareInfo.GetSortKey("e"),CompareInfo.GetSortKey("ĕ"));// returns < 0

Open question: Is this discrepancy allowable? I don't know if there's a valid scenario in which somebody uses GetSortKey under the invariant mode. But if there is, it might be necessary to make CompareInfo.Compare and SortKey.Compare agree on the result.

@GrabYourPitchforksGrabYourPitchforks added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

I wouldn't care much if someone using InvarantMode and Sort Keys. sort keys are really useful for scenarios that use real cultures and cares about linguistic behavior. The invariant mode is not for such scenarios at all.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Would it make sense for us to block the GetSortKey API under the invariant mode?

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Globalization/SortKey.cs Outdated
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Another possible solution (if we do care about CompareInfo and SortKey not getting out of sync on the same box) is to have logic akin to this in SortKey.Compare:

if(GlobalizationMode.Invariant){returnkey1._bytes.AsChars().SequenceCompareTo(key2._bytes.AsChars());}else{returnkey1._bytes.SequenceCompareTo(key2._bytes);}

@jkotas

Copy link
Copy Markdown
Member

there are endianness issues to consider.

Would it make sense to swap the bytes when creating the sort key in InvariantMode to fix this?

GlobalizationMode.Invariant mode as it exists today is not very useful unless you are building a microservices that does not ever use any globalization APIs. I do not think we need to worry about what SortKeys do in it too much. I am wondering whether we can make GlobalizationMode.Invariant to be more useful by making it to be roughly on par with the globalization stack that came with classic Mono. The data for it were still small, but it was actually reasonably useful. Some of this overlaps with your idea to include Unicode data with the runtime.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I do not think we need to worry about what SortKeys do in it too much.

Sounds like that's two votes for "don't bother with endianness concerns"? :)

@jkotas

Copy link
Copy Markdown
Member

That, or if you would like to write the few lines of code to swap endianess in the invariantmode sort key generation - that's fine too.

@pentp

pentp commented Feb 5, 2020

Copy link
Copy Markdown
Contributor

Making GlobalizationMode.Invariant more useful/practical sounds like a good idea. I've tried it on a (micro)service that should only use InvariantCulture (or en-US), but changing to GlobalizationMode.Invariant is still a bit scary because of its quirks and it's probably not a tested scenario for Orleans or AspNetCore or Newtonsoft.Json...

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Rename internal method InvariantToUpper to InvariantCaseFold so that it can be easily updated in the future
  • Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags
  • Simplify SortKey.Compare per PR feedback
  • Update InvariantCreateSortKey to be endianness-aware and use safer span-based logic in lieu of pointers

@GrabYourPitchforksGrabYourPitchforks removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 5, 2020
@tarekgh

Copy link
Copy Markdown
Member

Remove ValidHashCodeOfStringMaskOffFlags and ValidSortkeyCtorMaskOffFlags and fold them into ValidCompareMaskOffFlags, since Compare / GetHashCode / GetSortKey should all accept the same flags

GetHashCode doesn't allow CompareOptions.StringSort I think.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 5, 2020

Copy link
Copy Markdown
MemberAuthor

GetHashCode doesn't allow CompareOptions.StringSort I think.

It has to. GetHashCode is internally built on top of GetSortKey, which means that it has to allow the same set of parameters.

This same logic applies to Compare. In practice, Compare can be thought of as a glorified "get sort key for A" and "get sort key for B", then perform a byte-wise comparison of the two sort keys.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest iteration:

  • Suppress one test case on Win7, as LCMapStringEx and CompareStringEx return different results and it's throwing off the unit test. This is only cropping up now because I modified the SortKey.Compare unit test to also ensure that CompareInfo.Compare returns the same result.

@GrabYourPitchforks
GrabYourPitchforks merged commit 6766eb7 into dotnet:masterFeb 6, 2020
@GrabYourPitchforks
GrabYourPitchforks deleted the sortkey branch February 6, 2020 04:49
@ghostghost added the will_lock_this label Dec 6, 2020
@ghostghost locked as resolved and limited conversation to collaborators Jan 5, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GrabYourPitchforks@tarekgh@jkotas@pentp