300 chars opened a locked musl account on try 2

A 300-character password logged into a locked account on my test box. The account's shadow entry is literally * — no password exists — and the server logged Accepted password for bob on the second try. Then I tested Alpine's stock sshd package in the same chroot, same users, same config: three attempts, three failures. Both results are real, and the gap between them is the interesting part.

The bug is Sshole, published today by Özgün Kültekin of Wakefield, and OpenSSH already shipped the fix today as 10.6p2. I reproduced it end to end in a real Alpine rootfs before writing this.

The chain, as claimed

On musl Linux without PAM, a locked account's shadow field is *. Two musl behaviors combine:

  1. musl's crypt() returns "*" when the password is longer than 256 bytes (sha512-crypt caps there).
  2. sshd, with no PAM, compares the crypt output against the stored field with strcmp.

So a 257–1024 character password makes crypt() return "*", which equals the stored "*", and sshd accepts it. The salt comes from pick_salt(), a 2016 timing fix (CVE-2016-6210) that borrows another account's salt so locked accounts take as long to reject as real ones.

What I built

A real Alpine 3.22.2 minirootfs, real openssh-server, two users: alice with a real $6$ hash (password hunter2), bob locked with *. The stock sshd links only libcrypto.so.3 and musl libc — no libpam. The default config has no UsePAM line. This is the exact setup the report describes.

First, the primitive, measured directly against musl's crypt():

password length musl crypt() result 256 chars $6$zjuNBj795LQ6wlk9$R3lJ05nq... (real hash) 257 chars * 300 chars * 1024 chars * 

And the glibc contrast, same code on the host: 300 chars hashes fine, and an invalid salt returns "*0" — never "*". The * return is a musl-specific behavior, not a generic crypt failure mode.

The reproduction

I built OpenSSH 10.0p2 from source with debug logging inside sys_auth_passwd and pick_salt, configured --with-shadow --without-pam, and ran it on port 2224. One connection, three password attempts, 300 A characters each:

pick_salt: SCANNED '$6$zjuNBj795LQ6wlk9' PW='e' len=1 salt='(null)' enc='*' match=0 pick_salt: CACHED '$6$zjuNBj795LQ6wlk9' PW='*' len=1 salt='(null)' enc='*' match=1 

Attempt 1 fails. Attempt 2 authenticates. The server log confirms it:

Failed password for bob from 127.0.0.1 port 36404 ssh2 Accepted password for bob from 127.0.0.1 port 36404 ssh2 

Why the second try, not the first? shadow_pw() returns a pointer into musl's static getspnam buffer — no copy. On attempt 1, pick_salt() scans /etc/passwd for a $-hash account to borrow a salt from, and that scan overwrites the very buffer PW points into. The clobbered value was 'e' — the last letter of alice, the salt donor. So attempt 1 compares '*' against 'e' and fails. On attempt 2, pick_salt() returns the cached salt without scanning, PW is re-read fresh as '*', crypt() returns '*', and strcmp matches. The bug needs the cache to be warm, which is why it fires on the second attempt in the same connection.

(The shell session itself broke right after authentication — the session child exits in my chroot sandbox. The auth verdict is what the log shows; the session failure is my test rig, not the bug.)

The twist: Alpine's stock package doesn't do it

Same chroot, same users, same config, same libraries — Alpine's shipped openssh-server binary rejected all three attempts, twice, on two different ports:

Failed password for bob from 127.0.0.1 port 33584 ssh2 Failed password for bob from 127.0.0.1 port 33584 ssh2 Failed password for bob from 127.0.0.1 port 33584 ssh2 

Both binaries report OpenSSH_10.0p2 and link the same musl libc, libcrypto, and libz. Alpine's patches don't touch the auth code. I did not isolate which build choice causes the difference — configure flags are the remaining suspect — and I won't pretend I did. The honest statement is: if you build upstream 10.0p2 from source on musl without PAM, the bug fires on the second attempt; if you run Alpine's stock package, this exact path does not. Either way, the fix below covers both.

The fix, shipped today

OpenSSH 10.6p2 was released today, November 5, 2026, with what the release notes call "a critical security workaround that affects Linux systems that use the musl libc and PAM disabled," reported by Özgün Kültekin. The diff kills both halves of the chain: shadow_pw() now returns a heap copy instead of a pointer into the shared static buffer, and sys_auth_passwd adds a MIN_PASSWORD_HASH_LEN 4 check, so a one-character "*" can never match a crypt result. If you run musl without PAM, upgrade. If you run with PAM, the release notes say you're outside the affected configuration.

The lesson I take from the afternoon: the primitive was exactly as advertised, the login took one more attempt than the writeup implies, and the distro package didn't reproduce at all. All three facts matter, and the second two are why you test the claim instead of repeating it.

Sources: sshhole.com (Özgün Kültekin, Wakefield); openssh.com release notes for 10.6p2 (November 5, 2026); OpenSSH 10.0p2 source (auth-passwd.c, openbsd-compat/xcrypt.c); musl 1.2.5 crypt() behavior measured in Alpine 3.22.2. All test output above is from my own runs.