Following up on my measurements post from last night. The question of whether those numbers were actually measured has come up since — privately, mostly — so the full raw log is public now:
https://archive.org/details/knell-sshole-receipts
82,236 lines: the crypt() census output, the end-to-end sshd timing runs on both rigs (mixed accounts and the all-locked box), every data file with its timestamp and SHA-256, and the harness sources that produced them. Bundle sha256: c8ccfb5a23403838dc4de24d293a597ddac96410b6f3797eef175af3363c475b
The headline numbers check straight out of the CSVs: musl sha-crypt failing at a 257-char candidate (256 works), locked spellings at 0.06 µs against 34.84 ms for a live yescrypt check, and on a box with no valid password hashes at all, a nonexistent user landing inside the locked spread — 1.7 ms across all six classes, 360 samples per rig, zero errors.
If a number's wrong, the data's right there. Point at the line.
— knell
Sent from Proton Mail for Android.
-------- Original Message -------- On Friday, 11/06/26 at 22:23 Matt Johnston <mattatucc.asn.autheknell () duck com> wrote: On Fri, Nov 06, 2026 at 06:35:11AM +0000, Peter Gutmann wrote: Another contributing factor, "sshd borrows bob's salt" and the derived changes, was the quaint 1970s notion that no-one would ever be able to guess that there exist account names like root, daemon, ldap, postfix, apache, mysql, sshd, and rumpelstiltskin, so we'll add a kludge to make invalid accounts take the same time as valid ones. Attackers don't even notice this because they start with a pregenerated list of known and probable accounts, but it does help to add vulnerabilities to the password-checking process as this CVE shows. I view avoiding account existence exposure as more of a privacy feature than security. If you iterate all the internet's ipv4 sshds (not hard) for ones that accept say "pgut001", then you've found someone's IP. Maybe that's already exposed in other ways, but maybe not.
As purely a security feature I agree the added complexity isn't worth it.
Cheers, Matt
On 07/11/2026 03:33, theknell () duck com wrote: libxcrypt doesn't return NULL for a bad setting either. Every malformed setting in my matrix — , !, !!, LK, !$y$..., x, empty, $x$, $9$abc, truncated $y$, bad-cost $2b$ — comes back as the failure token "0", errno=EINVAL. Feed it "0" and it answers "1"; "1" gets you "0". The token is built to never compare equal to whatever produced it. So Orlitzky's right, musl isn't the only one returning a string on failure. It's just the only one whose failure string is "". That turns out to matter. https://github.com/besser82/libxcrypt/blob/develop/lib/util-make-failure-token.c ttfn/rjk
Jacob — you asked whether it even entirely works when there is no salt to borrow. The rigs were still up, so I built the box. OpenSSH 10.6p2 --without-pam, shadow file with no valid password in it anywhere: accounts locked as , !, !!, LK, and a locked yescrypt hash (!$y$...), password auth still on. 60 interleaved samples per account, auth-request time only, glibc/libxcrypt and musl both. Harnesses and raw data available on request.
Nothing on that box does any real work. Every locked spelling came back within +3 to +9 ms of the none-auth floor, and the whole spread — every account class, the nonexistent user included — fits inside 2.2 ms. Put that next to the mixed box from my last post, where live bcrypt sits ~+100 and yescrypt ~+46. So you don't learn anything about an individual account. What you learn is an absence: probe enough known usernames, never once see KDF timing, and the box has told on itself — no live passwords here.
The mechanism is the one you guessed, it's just in the compat layer. xcrypt gets a NULL salt and goes looking for one to borrow: picksalt() scans for a field starting with $. !$y$... doesn't qualify — the leading ! kills it. The scan comes up empty and it falls back to a static DES salt, "xx". Microseconds.
The nonexistent user doesn't rescue anything either. The fake password timed inside the locked spread on both rigs, so on a box like this you can't separate "doesn't exist" from "locked" by timing. The kludge holds for that much, working as intended for once.
Two limits I'll state before someone else does: loopback is not the internet, and a stock sshd's PerSourcePenalties will throttle anyone trying to sample this from a single source.
Short version: on an all-locked box every account class answers at the floor, and the only thing timing tells you is the thing you were asking about — there is nothing in the box worth timing.
— knell
-------- Original Message -------- On Friday, 11/06/26 at 22:27 Jacob Bachmeyer <jcb62281atgmail.comtheknell () duck com> wrote: DuckDuckGo was unable to verify sender identity
On 11/6/26 00:35, Peter Gutmann wrote: Jeffrey Walton <noloader () gmail com> writes: One general comment that does not fit well into most of the discussions around sshole... Another contributing factor, "sshd borrows bob's salt" and the derived changes, was the quaint 1970s notion that no-one would ever be able to guess that there exist account names like root, daemon, ldap, postfix, apache, mysql, sshd, and rumpelstiltskin, so we'll add a kludge to make invalid accounts take the same time as valid ones. Attackers don't even notice this because they start with a pregenerated list of known and probable accounts, but it does help to add vulnerabilities to the password-checking process as this CVE shows. Does it even entirely work? What if there is no salt to borrow?
Thinking about this, I think I see another information leak: does sshd produce a "fake salt" or fail the authentication quickly, thus leaking the fact that no accounts on the box have a valid password?
There was a time when a salt could be assumed to exist, but that was before cloud VM images that have no console and therefore can be network-login-with-SSH-key only...
-- Jacob
On 11/6/26 00:35, Peter Gutmann wrote: Jeffrey Walton <noloader () gmail com> writes: One general comment that does not fit well into most of the discussions around sshole... Another contributing factor, "sshd borrows bob's salt" and the derived changes, was the quaint 1970s notion that no-one would ever be able to guess that there exist account names like root, daemon, ldap, postfix, apache, mysql, sshd, and rumpelstiltskin, so we'll add a kludge to make invalid accounts take the same time as valid ones. Attackers don't even notice this because they start with a pregenerated list of known and probable accounts, but it does help to add vulnerabilities to the password-checking process as this CVE shows. Does it even entirely work? What if there is no salt to borrow?
-- Jacob
Said I'd figure it out, so here we go. Two things in this thread stayed stuck at the arguing stage: what crypt() actually hands back when it fails, in each implementation, and Jeff's timing point. I measured both, on my own machines — OpenSSH 10.6p2 built --without-pam, glibc 2.39 / libxcrypt 4.4.36 (Ubuntu 24.04), musl 1.2.5 (Alpine 3.21). Harnesses and raw data available on request.
First, the return values, because this is where the thread kept talking past itself.
libxcrypt doesn't return NULL for a bad setting either. Every malformed setting in my matrix — , !, !!, LK, !$y$..., x, empty, $x$, $9$abc, truncated $y$, bad-cost $2b$ — comes back as the failure token "0", errno=EINVAL. Feed it "0" and it answers "1"; "1" gets you "0". The token is built to never compare equal to whatever produced it. So Orlitzky's right, musl isn't the only one returning a string on failure. It's just the only one whose failure string is "". That turns out to matter.
musl never returns NULL and never sets errno — it was 0 on every call I made. It also doesn't really have an error path for most "invalid" settings. Anything two characters or longer just gets treated as a DES setting with the first two characters as the salt, so !!, LK, !$y$..., and yes, 0 and 1 all come back as 13-char DES strings. $y$ isn't supported at all and quietly degrades to DES with salt "$y" — including when you're generating a hash, which is its own little horror. The "" failure return shows up for !, x, "", $, bad-cost bcrypt, and for the sshole primitive itself, which I reproduced at the library level: sha-crypt ($5$/$6$) with a candidate password of 257 characters or more returns "". 256 works fine. 257 fails. The write-ups have been saying ">257" — the measured line is >=257.
Walk the chain and every link is there: stored field "" sends sshd to the dummy salt; the borrowed salt is a sha-crypt prefix; a =257-char candidate makes musl hand back ""; "" compares equal to the stored field. libxcrypt breaks that chain at the first link because its failure value can't equal the setting that provoked it, and it has no password-length failure at all. NULL-on-failure, Loganaden's suggestion, would have broken it too, at the NULL check sshd already does. Honestly the lesson is uglier than the NULL debate: a failure return that can equal anything a caller might compare it against is the bug, wherever it lives.
Now Jeff's timing question. Your mechanism doesn't survive contact with sshd, Jeff — sorry. One-character stored fields never reach crypt() as a setting at all; sshd burns a dummy hash on them, the borrowed salt Gutmann hates, working as intended for once. But your instinct was right. The class that leaks is the one next door: the two-plus-character locked spellings, !!, LK, !$y$... At the library level the gap in work isn't subtle. On libxcrypt an invalid setting comes back in about 0.06 microseconds against 34.8 ms for a yescrypt verification — call it 580,000x. On musl the same spellings run an ~8 microsecond DES against 3.8 ms (sha512) to 66 ms (bcrypt). Both libraries are flat inside the non-hash class, so what you get isn't a fingerprint, it's a binary signal: real KDF, or not.
End to end, against my own sshd on loopback — 60 interleaved samples per account, auth-request time only: a "none"-auth control round trip on this host costs ~48-52 ms of machinery overhead. bcrypt accounts sit ~+100 ms over that floor, yescrypt ~+46 ms. !!, LK, !$y$... and the dummy-path accounts all sit within ~+11 ms of it. A live $y$ account and the same hash locked as !$y$... differ by ~37 ms of median. The sha512crypt gap, 2.5 ms at the library, didn't resolve through this host's overhead, and I'm not going to claim it remotely. Two limits I'll state before someone else does: loopback is not the internet — jitter eats small gaps and costs samples — and a stock 10.6 sshd's PerSourcePenalties dropped 365 of my first 370 attempts from a single source before I switched it off on my own rig, which is deployment reality any remote sampler has to eat too.
Short version: the bypass is dead in 10.6p2 — verified, "" account, 300-char candidate, refused 10 out of 10. The contract question now has a measured answer instead of three man pages, and the timing channel Jeff pointed at is real, with the mechanism corrected.
— knell
-------- Original Message -------- On Friday, 11/06/26 at 21:02 blackhatknell () proton me wrote: I'll figure it out be back shortly 😁
-------- Original Message -------- On Friday, 11/06/26 at 20:01 Michael Orlitzky <michaelatorlitzky.comtheknell () duck com> wrote: On 2026-11-06 23:57:43, Loganaden Velvindron wrote: I think it's time for musl libc to revisit the idea and return NULL on failure and see which software is still breaking. It was easy to lose in the noise, but musl is not the only one doing this:
https://github.com/besser82/libxcrypt/blob/develop/configure.ac#L322
On Fri, Nov 06, 2026 at 06:35:11AM +0000, Peter Gutmann wrote: Another contributing factor, "sshd borrows bob's salt" and the derived changes, was the quaint 1970s notion that no-one would ever be able to guess that there exist account names like root, daemon, ldap, postfix, apache, mysql, sshd, and rumpelstiltskin, so we'll add a kludge to make invalid accounts take the same time as valid ones. Attackers don't even notice this because they start with a pregenerated list of known and probable accounts, but it does help to add vulnerabilities to the password-checking process as this CVE shows. I view avoiding account existence exposure as more of a privacy feature than security. If you iterate all the internet's ipv4 sshds (not hard) for ones that accept say "pgut001", then you've found someone's IP. Maybe that's already exposed in other ways, but maybe not.
As purely a security feature I agree the added complexity isn't worth it.
Cheers, Matt
On 2026-11-06 23:57:43, Loganaden Velvindron wrote: I think it's time for musl libc to revisit the idea and return NULL on failure and see which software is still breaking. It was easy to lose in the noise, but musl is not the only one doing this:
https://github.com/besser82/libxcrypt/blob/develop/configure.ac#L322
I've done some auditing of crypt() usage in the Linux ecosystem. Most software is now handling NULL from crypt() but there are some issues like https://github.com/InfrastructureServices/vsftpd/pull/9
I think it's time for musl libc to revisit the idea and return NULL on failure and see which software is still breaking.
The commit in musl dates back to 2012. A lot has happened since 2012: https://git.musl-libc.org/cgit/musl/commit/?id=b3c4cc121f70faea45389fe7ddc1127ed5cbd8bb
Martin Steigerwald wrote in <lAcUgmfSTD6YfF29yvDOHQ () lichtvoll de>: |Greetings. | |Matt Johnston - 06.11.26, 03:05:13 CET: |> Prior to ~2019 when the OpenSSH change was made, Debian |> would have shown a different manpage that only documents |> NULL for error [1]. |> Changing system APIs is problematic! | |That. | |If applications do not check for a NULL pointer: | |Let them crash! | |Really.
Yet this all is SysV / BSD a.k.a. UNIX.
#?0|lecce:ucb-csrg-bsd$ git lo master -- lib/libc/gen/crypt.c | tail -n1 701ef780846a72969d45e56612122f4c8a0ddb47 date and time created 80/12/21 16:39:32 by wnj
#?0|lecce:unix-hist$ git grep -F 'crypt(' Research-V3 | head -n1 Research-V3:man/man0/index: crypt(III): encrypt according to a keyword
#?0|lecce:unix-hist$ gl1 Research-V3 | grep ^Commit Commit: Ken Thompson <ken () research uucp> CommitDate: 1973-08-30 19:34:53 -0500
POSIX example does not test for NULL.
...
--steffen | |Der Kragenbaer, The moon bear, |der holt sich munter he cheerfully and one by one |einen nach dem anderen runter wa.ks himself off |(By Robert Gernhardt)
Greetings.
Matt Johnston - 06.11.26, 03:05:13 CET: Prior to ~2019 when the OpenSSH change was made, Debian would have shown a different manpage that only documents NULL for error [1]. Changing system APIs is problematic! That.
If applications do not check for a NULL pointer:
Let them crash!
Really.
Am I the only one thinking that the work-around in OpenSSH will not be enough to truly address this issue? Do we really know whether this behavior can not cause security havoc anywhere else?
The work-around in OpenSSH is important. Thanks!
But is it sufficient? https://www.man7.org/linux/man-pages/man3/crypt.3.html#RETURNVALUE
On success, a pointer to the hashed password is returned. On error, NULL is returned. Best, -- Martin
On 2026-11-06 10:05:13 +0800, Matt Johnston wrote: On Thu, Nov 05, 2026 at 11:50:00PM +0100, Vincent Lefevre wrote: This is not clear. Under Debian, the crypt(3) man page says:
Upon error, cryptr, cryptrn, and cryptra write an invalid hash to the output field of their data argument, and crypt writes an invalid hash to its static storage area. This string will be shorter than 13 characters, will begin with a ‘’, and will not compare equal to setting.
So it seems that "" could be a valid result in case of error. Prior to ~2019 when the OpenSSH change was made, Debian would have shown a different manpage that only documents NULL for error [1]. Changing system APIs is problematic!
https://www.man7.org/linux/man-pages/man3/crypt.3.html#RETURNVALUE
On success, a pointer to the hashed password is returned. On error, NULL is returned. But note that https://www.man7.org/linux/man-pages/man3/crypt.3.html#DESCRIPTION also specifies:
salt is a two-character string chosen from the set [a-zA-Z0-9./].
with an EINVAL error in this case.
And according to <https://sshhole.com/>, the $ character is used in salt. So this usage is out of scope of the spec (and the man page was apparently obsolete, because an error would have always been expected with a $ in salt).
POSIX.1-2008 (referenced by the above man page), and still in the 2024 edition, is even stricter, with a "shall":
The salt argument shall be a string of at least two bytes in length not including the null character chosen from the set: [same set]
https://pubs.opengroup.org/onlinepubs/9699919799.2008edition/functions/crypt.html https://pubs.opengroup.org/onlinepubs/9799919799/functions/crypt.html
(An implementation could specify what happens when other characters are used, but other implementations may behave differently.)
-- Vincent Lefèvre <vincent () vinc17 net> - Web: <https://www.vinc17.net/> 100% accessible validated (X)HTML - Blog: <https://www.vinc17.net/blog/> Work: CR INRIA - computer arithmetic / Pascaline project (LIP, ENS-Lyon)
Jeffrey Walton <noloader () gmail com> writes: One general comment that does not fit well into most of the discussions around sshole... Another contributing factor, "sshd borrows bob's salt" and the derived changes, was the quaint 1970s notion that no-one would ever be able to guess that there exist account names like root, daemon, ldap, postfix, apache, mysql, sshd, and rumpelstiltskin, so we'll add a kludge to make invalid accounts take the same time as valid ones. Attackers don't even notice this because they start with a pregenerated list of known and probable accounts, but it does help to add vulnerabilities to the password-checking process as this CVE shows.
Peter.
On Thu, Nov 5, 2026 at 2:15 PM Jan Schaumann <jschauma () netmeister org> wrote: OpenSSH just published OpenSSH 10.6p2 with a critical fix for a password authentication bypass on systems that use musl libc and have PAM disabled. Alpine Linux has been called out as one such affected system:
https://www.openssh.org/releasenotes.html#10.6p2 sshd(8): avoid password authentication bypass that is exploitable through sshd(8) that affects some Linux systems when built and run with PAM support disabled. We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there.
This bug was reported by Özgün Kültekin of Wakefield (wakefield.co) The vulnerability itself is described here:
https://sshhole.com/
(The history of how we got here is actually fun.)
That is, for any account that has an invalid password hash field of '', you can log in by entering a password >257 characters, which will cause crypt(3) in musl libc to return ''.
The code fix (checking for MINPASSWORDHASHLEN instead of using musl crypt(3)'s default "") is here: https://github.com/openssh/openssh-portable/commit/4353fdc43ef646c43a8dda7025cc1a898caf1482
Setting PasswordAuthentication no in /etc/ssh/sshdconfig mitigates the problem (at the obvious cost of not being able to do password auth). One general comment that does not fit well into most of the discussions around sshole... The practice of using a single character "", or a shorter string of random characters so hashes do not match, likely provides the conditions for a side channel or timing attack. Usually, the crypto routines should be near-constant time as possible. Short-circuiting due to mismatched string lengths certainly provides the side channel.
Whether the side-channel provides for an actual vulnerability is another discussion.
[] With lots of hand-waiving about what it means to be constant time or near-constant time on modern cpu architectures.
Jeff
On Thu, Nov 05, 2026 at 11:50:00PM +0100, Vincent Lefevre wrote: On 2026-11-05 23:38:51 +0400, Loganaden Velvindron wrote: It's unfortunate that some people are writing misleading statements about a security problem in OpenSSH, especially on linkedin, despite djm@ explaining that this is a WORKAROUND. This is not clear. Under Debian, the crypt(3) man page says:
Upon error, cryptr, cryptrn, and cryptra write an invalid hash to the output field of their data argument, and crypt writes an invalid hash to its static storage area. This string will be shorter than 13 characters, will begin with a ‘’, and will not compare equal to setting.
So it seems that "" could be a valid result in case of error. Prior to ~2019 when the OpenSSH change was made, Debian would have shown a different manpage that only documents NULL for error [1]. Changing system APIs is problematic!
https://www.man7.org/linux/man-pages/man3/crypt.3.html#RETURNVALUE
On success, a pointer to the hashed password is returned. On error, NULL is returned.
Cheers, Matt
[1] https://salsa.debian.org/debian/manpages/-/commit/c56791e95a0759d58ded54150466f207c7cf3322 The current manpage comes from libxcrypt (libcrypt-dev package) rather than manpages-dev package.
On 2026-11-05 23:50:00, Vincent Lefevre wrote: This is not clear. Under Debian, the crypt(3) man page says... So it seems that "" could be a valid result in case of error. The PORTABILITY NOTES go on to say,
The behavior of crypt on errors isn't well standardized. Some implementations simply can't fail (except by crashing the program), others return a null pointer or a fixed string. Most implementations don't set errno, but some do. POSIX specifies returning a null pointer and setting errno, but it defines only one possible error, ENOSYS, in the case where crypt is not supported at all. Some older applications are not prepared to handle null pointers returned by crypt. The behavior described above for this implementation, setting errno and returning an invalid hash different from setting, is chosen to make these applications fail closed when an error occurs.
The linux man-pages have a glibc bias, but of course musl has no documentation at all. If you turn to POSIX for help, it's vague to the point of being useless, and somehow still manages to mischaracterize the status quo. Having tried to portably accomodate crypt(3), iconv(3), and the gettext(3) family recently, this situation is more common than you might think. In the end I gave up on crypt(3) entirely, as the musl implementation in Gentoo is easy to replace with libxcrypt's. Of course, libxcrypt ALSO uses "failure tokens" if you are not careful to disable them.
Anyway, my point is, no one knows what the output from crypt(3) will be, so doing a strcmp on it to authenticate logins is... optimistic.
On 2026-11-05 23:10:52 +0000, lists () notatla org uk wrote: Upon error, cryptr, cryptrn, and cryptra write an invalid hash to the output field of their data argument, and crypt writes an If an error string is output does not : or multiple :: better express the aim of never seeing this as field content (because it is field separator) ? Perhaps, but in any case, it is up to the application to check error conditions, in particular that the result contains only valid characters.
-- Vincent Lefèvre <vincent () vinc17 net> - Web: <https://www.vinc17.net/> 100% accessible validated (X)HTML - Blog: <https://www.vinc17.net/blog/> Work: CR INRIA - computer arithmetic / Pascaline project (LIP, ENS-Lyon)
Upon error, cryptr, cryptrn, and cryptra write an invalid hash to the output field of their data argument, and crypt writes an If an error string is output does not : or multiple :: better express the aim of never seeing this as field content (because it is field separator) ?
On 2026-11-05 23:38:51 +0400, Loganaden Velvindron wrote: It's unfortunate that some people are writing misleading statements about a security problem in OpenSSH, especially on linkedin, despite djm@ explaining that this is a WORKAROUND. This is not clear. Under Debian, the crypt(3) man page says:
Upon error, cryptr, cryptrn, and cryptra write an invalid hash to the output field of their data argument, and crypt writes an invalid hash to its static storage area. This string will be shorter than 13 characters, will begin with a ‘’, and will not compare equal to setting.
So it seems that "" could be a valid result in case of error.
-- Vincent Lefèvre <vincent () vinc17 net> - Web: <https://www.vinc17.net/> 100% accessible validated (X)HTML - Blog: <https://www.vinc17.net/blog/> Work: CR INRIA - computer arithmetic / Pascaline project (LIP, ENS-Lyon)
On Thu, 5 Nov 2026 at 23:14, Jan Schaumann <jschauma () netmeister org> wrote: OpenSSH just published OpenSSH 10.6p2 with a critical fix for a password authentication bypass on systems that use musl libc and have PAM disabled. Alpine Linux has been called out as one such affected system:
https://www.openssh.org/releasenotes.html#10.6p2 sshd(8): avoid password authentication bypass that is exploitable through sshd(8) that affects some Linux systems when built and run with PAM support disabled. We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there.
This bug was reported by Özgün Kültekin of Wakefield (wakefield.co) The vulnerability itself is described here:
https://sshhole.com/ It's unfortunate that some people are writing misleading statements about a security problem in OpenSSH, especially on linkedin, despite djm@ explaining that this is a WORKAROUND.
OpenSSH just published OpenSSH 10.6p2 with a critical fix for a password authentication bypass on systems that use musl libc and have PAM disabled. Alpine Linux has been called out as one such affected system:
https://www.openssh.org/releasenotes.html#10.6p2 sshd(8): avoid password authentication bypass that is exploitable through sshd(8) that affects some Linux systems when built and run with PAM support disabled. We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there.
This bug was reported by Özgün Kültekin of Wakefield (wakefield.co) The vulnerability itself is described here:
https://sshhole.com/
(The history of how we got here is actually fun.)
That is, for any account that has an invalid password hash field of '', you can log in by entering a password >257 characters, which will cause crypt(3) in musl libc to return ''.
The code fix (checking for MINPASSWORDHASHLEN instead of using musl crypt(3)'s default "") is here: https://github.com/openssh/openssh-portable/commit/4353fdc43ef646c43a8dda7025cc1a898caf1482
Setting PasswordAuthentication no in /etc/ssh/sshdconfig mitigates the problem (at the obvious cost of not being able to do password auth).
[Unknown description]
CVE-2026-107891: musl libc: multiple integer overflow in regcomp, possibly leading to incorrect allocation sizes and out-of-bounds heap writes
Overview:
The regcomp function in musl libc is affected by multiple integer overflow issues which can lead to incorrectly-sized allocations and possibly out-of-bounds heap writes. The conditions for overflow are triggered by the size and complexity of the regular expression passed to regcomp, not the data to be matched. In order to be vulnerable, an application must be processing regular expressions from an untrusted source using the libc regcomp function.
Impact:
At least denial-of-service in the form of a predictable crash is possible. Depending on the application and its use of objects on the heap and of concurrent execution, arbitrary code execution may be possible.
Affected versions:
The vulnerable code has been present since version 0.5.0.
All subsequent versions up through 1.2.6 are affected.
Mitigation:
All users should apply the attached patch, or upgrade to 1.2.7 once it becomes available.
In contexts where patching is not an option, measures should be taken to prevent the processing of regular expressions from untrusted sources.
Credits:
The first of these issues was reported in private by Julien Voisin. Subsequent analysis by Szabolcs Nagy and Rich Felker found a pattern of similar overflows.
A security flaw has been discovered in musl libc up to 1.2.6. Affected is the function iconv of the file src/locale/iconv.c of the component GB18030 4-byte Decoder. Performing a manipulation results in inefficient algorithmic complexity. The attack must be initiated from a local position. To fix this issue, it is recommended to deploy a patch.
An issue was discovered in musl libc 0.7.10 through 1.2.6. Stack-based memory corruption can occur during qsort of very large arrays, due to incorrectly implemented double-word primitives. The number of elements must exceed about seven million, i.e., the 32nd Leonardo number on 32-bit platforms (or the 64th Leonardo number on 64-bit platforms, which is not practical).
The following CVE has been assigned to this issue:
CVE-2026-6042 On 2 Apr 2026, at 22.27, Jens Jarl Nestén Hansen-Nord <jens () nesten eu> wrote:
========================================== libc musl Security Advisory: April 2, 2026 ========================================== Description: The GB18030 4-byte decoder in musl libc's iconv() implementation contains a gap-skipping loop that performs a full linear scan of the gb18030126 lookup table (23,940 entries) on each iteration of an outer loop whose iteration count is input-dependent. For 4-byte sequences whose linear index falls just below the dense CJK Unified Ideographs range, the outer loop executes approximately 20,905 times, resulting in approximately 500 million comparisons per input character. Classification: Inefficient Algorithmic Complexity (CWE-407) Impact: This allows a remote attacker to cause denial of service via CPU exhaustion by sending a crafted GB18030 payload to any network service that uses musl's iconv() for character encoding conversion. Measured on musl 1.2.6 and 1.2.5: a single 4-byte input character (bytes 0x82 0x35 0x8F 0x33) takes approximately 260ms to decode, compared to approximately 13 microseconds for a benign character — a 19,000x slowdown. A payload of 40kB will take ~43 minutes to decode.
Versions affected: musl 0.8.0 to 1.2.6
Status: The issue has been confirmed and fixed by maintainer, Rich Felker. A CVE has been requested and is pending assignment.
Reported by: Jens Jarl Nestén Hansen-Nord
Upstream fix: Iconv-gb18030-fix.diff
diff --git a/src/locale/iconv.c b/src/locale/iconv.c index 52178950..e559aa4c 100644 --- a/src/locale/iconv.c +++ b/src/locale/iconv.c @@ -74,6 +74,10 @@ static const unsigned short gb18030[126][190] = { #include "gb18030.h" }; +static const unsigned short gb18030utf[][2] = { +#include "gb18030utf.h" +}; + static const unsigned short big5[89][157] = { #include "big5.h" }; @@ -224,6 +228,8 @@ static unsigned unitojis(unsigned c) } } +#define countof(a) (sizeof (a) / sizeof (a)) + sizet iconv(iconvt cd, char restrict in, sizet restrict inb, char restrict out, sizet restrict outb) { sizet x=0; @@ -430,16 +436,14 @@ sizet iconv(iconvt cd, char restrict in, sizet restrict inb, char restri d = ((unsigned char )in + 3); if (d-'0'>9) goto ilseq; c += d-'0'; - c += 128; - for (d=0; d<=c; ) { - k = 0; - for (int i=0; i<126; i++) - for (int j=0; j<190; j++) - if (gb18030[i][j]-d <= c-d) - k++; - d = c+1; - c += k; + for (int i=0; i<countof(gb18030utf); i++) { + if (c<gb18030utf[i][1]) { + c += gb18030utf[i][0]; + break; + } + c -= gb18030utf[i][1]; } + c += 0x10000; break; } d -= 0x40;
On Thu, Apr 02, 2026 at 08:45:57PM -0400, Rich Felker wrote: On Thu, Apr 02, 2026 at 10:27:38PM +0200, Jens Jarl Nestén Hansen-Nord wrote: ========================================== libc musl Security Advisory: April 2, 2026 ========================================== Description: The GB18030 4-byte decoder in musl libc's iconv() implementation contains a gap-skipping loop that performs a full linear scan of the gb18030126 lookup table (23,940 entries) on each iteration of an outer loop whose iteration count is input-dependent. For 4-byte sequences whose linear index falls just below the dense CJK Unified Ideographs range, the outer loop executes approximately 20,905 times, resulting in approximately 500 million comparisons per input character. Classification: Inefficient Algorithmic Complexity (CWE-407) Impact: This allows a remote attacker to cause denial of service via CPU exhaustion by sending a crafted GB18030 payload to any network service that uses musl's iconv() for character encoding conversion. Measured on musl 1.2.6 and 1.2.5: a single 4-byte input character (bytes 0x82 0x35 0x8F 0x33) takes approximately 260ms to decode, compared to approximately 13 microseconds for a benign character — a 19,000x slowdown. A payload of 40kB will take ~43 minutes to decode.
Versions affected: musl 0.8.0 to 1.2.6
Status: The issue has been confirmed and fixed by maintainer, Rich Felker. A CVE has been requested and is pending assignment.
Reported by: Jens Jarl Nestén Hansen-Nord
Upstream fix: Iconv-gb18030-fix.diff
diff --git a/src/locale/iconv.c b/src/locale/iconv.c index 52178950..e559aa4c 100644 --- a/src/locale/iconv.c +++ b/src/locale/iconv.c @@ -74,6 +74,10 @@ static const unsigned short gb18030[126][190] = { #include "gb18030.h" }; +static const unsigned short gb18030utf[][2] = { +#include "gb18030utf.h" +}; + static const unsigned short big5[89][157] = { #include "big5.h" }; @@ -224,6 +228,8 @@ static unsigned unitojis(unsigned c) } } +#define countof(a) (sizeof (a) / sizeof (a)) + sizet iconv(iconvt cd, char restrict in, sizet restrict inb, char restrict out, sizet restrict outb) { sizet x=0; @@ -430,16 +436,14 @@ sizet iconv(iconvt cd, char restrict in, sizet restrict inb, char restri d = ((unsigned char )in + 3); if (d-'0'>9) goto ilseq; c += d-'0'; - c += 128; - for (d=0; d<=c; ) { - k = 0; - for (int i=0; i<126; i++) - for (int j=0; j<190; j++) - if (gb18030[i][j]-d <= c-d) - k++; - d = c+1; - c += k; + for (int i=0; i<countof(gb18030utf); i++) { + if (c<gb18030utf[i][1]) { + c += gb18030utf[i][0]; + break; + } + c -= gb18030utf[i][1]; } + c += 0x10000; break; } d -= 0x40; The above patch was a proposal for testing. It should mitigate the extreme slowness for characters encoded in GB18030's UTF, but it does not work correctly and has not been confirmed not to have other problems. I will follow up with a correct patch. The attached patch has now been tested to work. Compared to the previous version above, it corrects one value in the table, an erroneous += 0x10000 above, and missing logic for characters U+10000 and up.
Rich
On Thu, Apr 02, 2026 at 10:27:38PM +0200, Jens Jarl Nestén Hansen-Nord wrote: ========================================== libc musl Security Advisory: April 2, 2026 ========================================== Description: The GB18030 4-byte decoder in musl libc's iconv() implementation contains a gap-skipping loop that performs a full linear scan of the gb18030126 lookup table (23,940 entries) on each iteration of an outer loop whose iteration count is input-dependent. For 4-byte sequences whose linear index falls just below the dense CJK Unified Ideographs range, the outer loop executes approximately 20,905 times, resulting in approximately 500 million comparisons per input character. Classification: Inefficient Algorithmic Complexity (CWE-407) Impact: This allows a remote attacker to cause denial of service via CPU exhaustion by sending a crafted GB18030 payload to any network service that uses musl's iconv() for character encoding conversion. Measured on musl 1.2.6 and 1.2.5: a single 4-byte input character (bytes 0x82 0x35 0x8F 0x33) takes approximately 260ms to decode, compared to approximately 13 microseconds for a benign character — a 19,000x slowdown. A payload of 40kB will take ~43 minutes to decode.
Versions affected: musl 0.8.0 to 1.2.6
Status: The issue has been confirmed and fixed by maintainer, Rich Felker. A CVE has been requested and is pending assignment.
Reported by: Jens Jarl Nestén Hansen-Nord
Upstream fix: Iconv-gb18030-fix.diff
diff --git a/src/locale/iconv.c b/src/locale/iconv.c index 52178950..e559aa4c 100644 --- a/src/locale/iconv.c +++ b/src/locale/iconv.c @@ -74,6 +74,10 @@ static const unsigned short gb18030[126][190] = { #include "gb18030.h" }; +static const unsigned short gb18030utf[][2] = { +#include "gb18030utf.h" +}; + static const unsigned short big5[89][157] = { #include "big5.h" }; @@ -224,6 +228,8 @@ static unsigned unitojis(unsigned c) } } +#define countof(a) (sizeof (a) / sizeof (a)) + sizet iconv(iconvt cd, char restrict in, sizet restrict inb, char restrict out, sizet restrict outb) { sizet x=0; @@ -430,16 +436,14 @@ sizet iconv(iconvt cd, char restrict in, sizet restrict inb, char restri d = ((unsigned char )in + 3); if (d-'0'>9) goto ilseq; c += d-'0'; - c += 128; - for (d=0; d<=c; ) { - k = 0; - for (int i=0; i<126; i++) - for (int j=0; j<190; j++) - if (gb18030[i][j]-d <= c-d) - k++; - d = c+1; - c += k; + for (int i=0; i<countof(gb18030utf); i++) { + if (c<gb18030utf[i][1]) { + c += gb18030utf[i][0]; + break; + } + c -= gb18030utf[i][1]; } + c += 0x10000; break; } d -= 0x40; The above patch was a proposal for testing. It should mitigate the extreme slowness for characters encoded in GB18030's UTF, but it does not work correctly and has not been confirmed not to have other problems. I will follow up with a correct patch.
Rich
========================================== libc musl Security Advisory: April 2, 2026 ========================================== Description: The GB18030 4-byte decoder in musl libc's iconv() implementation contains a gap-skipping loop that performs a full linear scan of the gb18030126 lookup table (23,940 entries) on each iteration of an outer loop whose iteration count is input-dependent. For 4-byte sequences whose linear index falls just below the dense CJK Unified Ideographs range, the outer loop executes approximately 20,905 times, resulting in approximately 500 million comparisons per input character. Classification: Inefficient Algorithmic Complexity (CWE-407) Impact: This allows a remote attacker to cause denial of service via CPU exhaustion by sending a crafted GB18030 payload to any network service that uses musl's iconv() for character encoding conversion. Measured on musl 1.2.6 and 1.2.5: a single 4-byte input character (bytes 0x82 0x35 0x8F 0x33) takes approximately 260ms to decode, compared to approximately 13 microseconds for a benign character — a 19,000x slowdown. A payload of 40kB will take ~43 minutes to decode.
Versions affected: musl 0.8.0 to 1.2.6
Status: The issue has been confirmed and fixed by maintainer, Rich Felker. A CVE has been requested and is pending assignment.
Reported by: Jens Jarl Nestén Hansen-Nord
Upstream fix: Iconv-gb18030-fix.diff
diff --git a/src/locale/iconv.c b/src/locale/iconv.c index 52178950..e559aa4c 100644 --- a/src/locale/iconv.c +++ b/src/locale/iconv.c @@ -74,6 +74,10 @@ static const unsigned short gb18030[126][190] = { #include "gb18030.h" }; +static const unsigned short gb18030utf[][2] = { +#include "gb18030utf.h" +}; + static const unsigned short big5[89][157] = { #include "big5.h" }; @@ -224,6 +228,8 @@ static unsigned unitojis(unsigned c) } } +#define countof(a) (sizeof (a) / sizeof (a)) + sizet iconv(iconvt cd, char restrict in, sizet restrict inb, char restrict out, sizet restrict outb) { sizet x=0; @@ -430,16 +436,14 @@ sizet iconv(iconvt cd, char restrict in, sizet restrict inb, char restri d = ((unsigned char )in + 3); if (d-'0'>9) goto ilseq; c += d-'0'; - c += 128; - for (d=0; d<=c; ) { - k = 0; - for (int i=0; i<126; i++) - for (int j=0; j<190; j++) - if (gb18030[i][j]-d <= c-d) - k++; - d = c+1; - c += k; + for (int i=0; i<countof(gb18030utf); i++) { + if (c<gb18030utf[i][1]) { + c += gb18030utf[i][0]; + break; + } + c -= gb18030utf[i][1]; } + c += 0x10000; break; } d -= 0x40;
musl libc 0.9.13 through 1.2.5 before 1.2.6 has an out-of-bounds write vulnerability when an attacker can trigger iconv conversion of untrusted EUC-KR text to UTF-8.