Changes in libraries have a risk of breaking things in unpredictable places. Debugging a crash is usually simple. But recently, in the late stages of working on the curl
utility for Red Hat Enterprise Linux 9, I encountered a bug report that looked rather strange. In this article, I will discuss how this report led me to implement a localization bug fix during the late stages of RHEL 9 development.
Why the curl utility with the Turkish locale failed
The curl
utility failed in a system configured with the Turkish locale (tr_TR.utf8
). The utility crashed in RHEL 9. It did not crash in Fedora 36; however, it failed to establish HTTPS connections.
It was easy to determine that the crash occurred when fetching one of the cipher algorithms—camellia-128. This was strange because the cipher was in the OpenSSL default provider, so it should be always available. The only suspicious thing was that the lookup name was in all caps: CAMELLIA-128
. However, OpenSSL makes a case-insensitive comparison for these fetches. The crash was related to a check that was absent in OpenSSL 3.0.1 (the base version in RHEL 9), which also explained the behavior difference between Fedora and RHEL.
Since the crash was locale-specific and presumably related to uppercase/lowercase comparison, I explored the case-insensitive comparison and the Turkish language variations as possible factors. Searching online yielded many resources, but this article gives the best explanation. The alphabet used for English has a lowercase dotted i and an uppercase dotless I. But the Turkish alphabet has four letters for I: uppercase and lowercase dotted, and uppercase and lowercase dotless.
These additional Turkish letters change the relationship between the two letters English uses for I. Instead of the English case mapping of the lower dotted i to the upper dotless I, Turkish maps the lower dotted i to the upper dotted İ, and the lower dotless ı to the upper dotless I.
As it turns out, the change in the case rules for the letter i frequently breaks software logic. As the article puts it, "Applications that have been internationalized are conditioned to easily accept new characters, collations, case rules, encodings, and other character-based properties and relationships. However, their design often does not anticipate that the properties or rules of English letters will change."
Armed with this knowledge, I set out to debug the utility for the Turkish locale.
Match fails due to Turkish translations
In a case-insensitive comparison in the Turkish locale, the uppercase name CAMELLIA
used for the lookup becomes lowercase with no dot: camellıa.
Since camellıa does not match the expected string (camellia
), the algorithm cannot be not found. Therefore, the omitted NULL check explains the crash.
The comparison uses strcasecmp
and strncasecmp
functions, which are locale-dependent. The setlocale
function will trigger the problem, but the command-line OpenSSL utility does not invoke that function, so the problem doesn't arise in the upstream test suite. curl
uses the system locale for this purpose. This issue did not occur in earlier versions of OpenSSL because it did not use a case-insensitive string comparison before version 3. Now, new versions use case-insensitive string comparisons more extensively.
So why was the connection not established even when the system did not crash? A debugging session revealed that the cause was another change between OpenSSL 1.1.1 and 3.0. The culprit was the so-called encoders/decoders mechanism for finding a relevant parser of ASN.1 structures. Encoders and decoders also identify objects by name. This time, there was a mismatch between SubjectPublicKeyInfo
and subjectpublickeyinfo
.
If we want case insensitive comparison, we must always compare strings using a locale with predictable properties. Since algorithm names will never be internationalized, we can stop using locale-dependent strcasecmp
and strncasecmp
functions. We could compare strings byte-to-byte, but the profiling showed a significant performance slowdown. Luckily, POSIX implements a strcasecmp_l
function that accepts a specific locale object. If we pass a locale object matching the C locale to that function, we get the correct results.
Adopt a debugging process early
In theory, the rest of the process is simple: initialize a global object matching the C locale; use it everywhere in the OpenSSL code; free on shutdown. However, this leads to problems with the OpenSSL Federal Information Processing Standards (FIPS) provider. By design, FIPS requirements should not rely on a global object external to the module itself. This means we have to create another object inside the provider and manage it locally.
The resulting patch is one of the most invasive I have ever written. It changes approximately 80 files in the OpenSSL source tree and required serious testing. Global find-and-replace completes most of the changes. But some of them affect the core functionality of OpenSSL and, as mentioned before, touch the FIPS module. In order for it to be accepted upstream, we had to build it to accommodate non-Linux platforms as well, and that required additional changes. We had a long discussion with the upstream developers on how to include the patch in the stable 3.0 series
During the course of preparing this article for publication, there were long discussions within upstream and the community. As a result, upstream got rid of the locale object and switched the implementation to byte-to-byte comparison. We are going to switch our implementation in the same way, but that won't happen soon.
You may come across internationalization-related issues everywhere, even if you think that you rely on plain ASCII. A wide client base and early adoption could help you find a particular issue early on when it is significantly less burdensome.