Dear GNU Libidn maintainers, I found a use of uninitialized memory in Libidn's ToUnicode round-trip verification while fuzzing the project with DMSAN, my sanitizer tool for detecting uses of uninitialized memory. The issue is in the `lib/idna.c` library code; I use `idna_to_unicode_8z8z()` below because it is a small public API path that reaches the affected code.
The root cause is in `idna_to_unicode_internal()`. It removes the `xn--`
prefix, decodes the remaining Punycode suffix, calls `idna_to_ascii_4i()` into
a local stack buffer, and then compares against `tmpout +
strlen(IDNA_ACE_PREFIX)`:
```c
char tmpout[64];
rc = idna_to_ascii_4i(out, *outlen, tmpout, flags);
if (c_strcasecmp(utf8in, tmpout + strlen(IDNA_ACE_PREFIX)) != 0)
return IDNA_ROUNDTRIP_VERIFY_ERROR;
```
The code assumes that `idna_to_ascii_4i()` wrote an ACE string beginning with
`xn--` into `tmpout`. That assumption is false when the decoded label is all
ASCII, because the ToASCII path leaves ASCII labels unchanged:
```c
for (i = 0; src[i]; i++) {
if (src[i] > 0x7F)
inasciirange = 0;
if (i < 64)
out[i] = src[i];
}
if (i < 64)
out[i] = '\0';
else
return IDNA_INVALID_LENGTH;
if (inasciirange)
goto step8;
```
If the decoded ASCII label is shorter than 4 characters, `tmpout +
strlen("xn--")` points past the initialized string into the uninitialized tail
of `tmpout`. `c_strcasecmp()` then reads and branches on stale stack bytes.
In the included PoC, an earlier label in the same domain leaves matching bytes
in the same stack slot. A later invalid ACE label that should remain unchanged
is then decoded as a short ASCII label.
Native output from the primary PoC shows:
```text
input: xn--com-
expected: xn--com-
actual: xn--com-
rc: 0
OK
input: xn--XXXXcom--.xn--com-
expected: xn--XXXXcom--.xn--com-
actual: xn--XXXXcom--.com
rc: 0
BUG: output changed
input: xn--XXXXabcd--.xn--abcd-
expected: xn--XXXXabcd--.xn--abcd-
actual: xn--XXXXabcd--.xn--abcd-
rc: 0
OK
```
The middle case is the bug: the second label is `xn--com-`, which does not pass
ToUnicode round-trip verification and should stay unchanged. Instead, the
output label becomes `com`. The `abcd` case stays unchanged because the decoded
ASCII label is long enough that `tmpout + 4` no longer points into the
uninitialized tail.
The attack scenario is a hostname or email domain supplied to an application
that normalizes user input with Libidn ToUnicode before making a security
decision. The victim application may use the normalized domain for an allowlist
or blocklist check, routing, cache identity, logging, or a warning shown to
users. With default flags, the concrete effect is that an invalid ACE label
such as `xn--com-`, `xn--net-`, `xn--org-`, `xn--www-`, `xn--api-`, `xn--ssh-`,
or `xn--123-` can be normalized to `com`, `net`, `org`, `www`, `api`, `ssh`, or
`123` when matching stale stack bytes are present.
The primary PoC uses one domain string where an earlier label supplies those
bytes. I also observed that the same stale bytes can carry across separate API
calls in native builds using gcc. For example, after one call processes
`xn--XXXXwww--`, a later separate call processing `xn--www-` can return `www`;
after `xn--XXXX123--`, a later `xn--123-` can return `123`. This reproduced
with the 1.38 and 1.43 release tarballs and with current git `ef65bfd`. This
behavior is stack layout dependent, but it is relevant for long-running
processes that normalize multiple names supplied by an attacker. The attached
`poc_cross_call.c` demonstrates it. The reproduction uses native Libidn builds
and public APIs.
The same source pattern is visible at least as far back as the public tag
`libidn-0-1-15` from 2003-06-07 and remains present through `v1.43` from
2025-03-21 and current git `ef65bfd` from 2026-03-22. Early releases used
`idna.c` at the repository root and named the helper `idna_to_ascii()`, but the
same stack buffer and `tmpout + strlen(IDNA_ACE_PREFIX)` comparison were
already present. I directly reproduced the behavior on Ubuntu `libidn12
1.38-4ubuntu1` (`libidn.so.12.6.3`) and on a build from current source.
I checked current Libidn2 source as well; its A-label round-trip check is
implemented in different code and I did not find the same `tmpout +
strlen(IDNA_ACE_PREFIX)` pattern there.
I am not claiming code execution, invalid writes, native crashes, or heap
disclosure. Concretely, this is a use of uninitialized memory (CWE-457),
detected by DMSAN. It can still affect callers that trust Libidn's normalized
domain string for security decisions. I defer to your judgment on the final
classification.
My suggested fix is to compare the full original ACE label against the full
ToASCII result, instead of assuming the ToASCII result begins with `xn--`. RFC
3490 ToUnicode step 7 describes comparing the ToASCII result with the saved
copy from step 3, which is the full original label before removing the ACE
prefix. A smaller defensive check would be to reject the round-trip
verification if `tmpout` does not start with `IDNA_ACE_PREFIX` before using
`tmpout + strlen(IDNA_ACE_PREFIX)`:
```c
if (c_strncasecmp(tmpout, IDNA_ACE_PREFIX, strlen(IDNA_ACE_PREFIX)) != 0 ||
c_strcasecmp(utf8in, tmpout + strlen(IDNA_ACE_PREFIX)) != 0)
return IDNA_ROUNDTRIP_VERIFY_ERROR;
```
I would not recommend simply initializing `tmpout` to zero; that avoids the
uninitialized read but does not address the underlying assumption that the
ToASCII result always contains the ACE prefix.
The attached package contains the PoCs and a README with native and optional
Docker reproduction commands. The reproduction uses public Libidn APIs.
Could you please review whether you consider this a Libidn bug and whether this
classification seems reasonable? If you consider it security relevant, would
you also be willing to help request a CVE? If fixed, I would appreciate
attribution as "Reported by DMSAN (Differential Memory Sanitizer)". In my
paper, this would appear as one entry in the evaluation, described as a use of
uninitialized memory in Libidn's ToUnicode round-trip verification, without
overstating the impact.
Best,
Jiami (DMSAN)
libidn_tounicode_uninitialized_memory_pocs_20260519.tar.gz
Description: GNU Zip compressed data
