The password file of 3BSD, as it stood in 1980, gives the account daemon a single star where the hash belongs, and in the name field somebody wrote "The devil himself". A star cannot be the output of any hash function, so nothing typed at a prompt will ever reproduce it, and Unix has marked accounts that must never take a password in exactly this way ever since.
Forty-six years later Özgün Kültekin of Wakefield built sshd against glibc and against musl, each with PAM and without, and threw some 17,600 odd logins at the results. Every build agreed with every other build on every login, apart from one. sshd on musl without PAM accepted a password of three hundred characters for an account whose password field held nothing but that star.
Three sensible decisions
The first was made by musl, the C library under Alpine, on 9 August 2012. The Single UNIX Specification of 1997 says that crypt() on failure "returns a null pointer", and Rich Felker had found that plenty of software never checks. In the words of his commit, "a large portion of programs which call crypt are not prepared for its failure and do not check that the return value is non-null before using it." So musl's crypt() stopped returning a null pointer and returned "an unmatchable hash" instead, so that a careless program would see a wrong password and carry on. The string chosen for it was *. Twenty days later SHA-512 crypt arrived in musl with a limit of 256 bytes on the password, a limit its source describes as "added for DoS protection", since the work grows with the square of the key's length.
The second came from OpenSSH in July 2016. An sshd asked about an account that did not exist used to answer at once, because there was nothing to hash, and a stopwatch could tell real users from invented ones (CVE-2016-6210). Darren Tucker's fix of 15 July 2016 makes sshd hash the attempt regardless, with a salt borrowed from another account on the machine, so that every refusal takes the same time. Any account whose stored field is too short to contain a salt goes the same way, and a field of one star is rather short.
The third is Alpine's. Its openssh-server package is configured --without-pam, so sshd checks passwords itself, with crypt(), against the shadow file. Since about Alpine 3.16 the base image ships root as root:*, and a key-only user created with adduser and no password carries the same star. The official tiny-cloud images from 3.18.11 to 3.24.3 add a user called alpine, starred as well, with passwordless doas.
Each of these would pass any review on its own merits, and for ten years nobody reviewed them together.
Where the stars meet
Take a machine with one account whose field is a star and one other account with a real $6$ hash. An attacker sends a password of 300 characters for the starred account. sshd finds no salt in a field of one character and borrows the $6$ salt of the other account. musl's SHA-512 code refuses the password, because 300 is more than 256, and crypt() reports the failure in the way it was taught in 2012, with a star. sshd compares that star with the star in the shadow file, finds them identical, and lets the visitor in. The upper end of the window, 1,024 characters, is sshd's own limit on password length.
Getting there takes a spot of preparation. The attacker needs the name of a starred account and needs one ordinary hash somewhere on the same machine. Root itself stays out of reach as long as PermitRootLogin keeps its default, prohibit-password. The alpine user on a cloud image with passwordless doas reaches root by another door, which on those images makes the default somewhat academic. Alpine's own advisory sums up the result plainly: "An attacker who can reach your SSH port can log in without knowing any password. It takes one connection, works every time, and needs no retries." For a daemon whose whole job is to say no, that is not on.
OpenSSH released 10.6p2 on 5 November with a workaround, and its release notes place the blame with some care: "We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there." The stable branch now compares a password only against a stored field of at least four characters, which a star is not. Alpine published fixed packages the same day for every branch from 3.16 to edge, including 3.21, whose support had ended four days earlier, and reminded its users that "Upgrading the package alone does not replace the running daemon."
None of the three projects had a bug in the sense its own tests could see. As the researcher notes, OpenSSH is tested on OpenBSD and on glibc, while musl does not test sshd at all. Most distributions, the researcher adds, put PAM in front as well, and PAM never asks crypt() about a star. The combination had been in place since 2016, assembled by a distribution out of parts that had never met in anybody's test suite.
The same login on FreeBSD
FreeBSD took OpenSSH into the base system with release 4.0 in March 2000 ("OpenSSH 1.2 has been integrated with the base system"), and it has built it ever since against its own C library and OpenPAM, a PAM library written for the FreeBSD Project. The configuration that ships is FreeBSD's own, and the file in the tree says so: "Note that some of FreeBSD's defaults differ from OpenBSD's".
The attacker's 300 characters meet the first of those defaults straight away. sshd_config(5) on FreeBSD 15.0 gives PasswordAuthentication the default "no" and UsePAM the default "yes", so sshd never performs its own crypt() comparison; a password reaches the system only through keyboard-interactive authentication and PAM.
Suppose an administrator switches both, password authentication on and PAM off, which puts sshd on precisely the code path that failed on Alpine. I read crypt.c and crypt-sha512.c in the releng/15.0 tree this morning to see what happens next. The SHA-512 code limits the salt to 16 characters and the rounds to a maximum, and sets no limit on the password, so 300 characters produce an ordinary $6$ hash, which does not equal a star. And where crypt() does fail, crypt.c returns NULL, as the manual page states in so many words: "NULL on failure". sshd's comparison begins with encrypted_password != NULL. OpenSSH relied on the old rule from the standard, and FreeBSD's library kept it.
Both answers come from one project. The C library, the PAM library, sshd and its configuration sit in the same tree and leave in the same release, so a decision about what crypt() says on failure is taken by people who can see every caller in the system. A library that serves whatever a distribution later compiles against it has to guess what its callers do, and in 2012 musl guessed in the caller's favour.
The limit
One tree does not make OpenSSH free of bugs. FreeBSD shipped FreeBSD-SA-24:04.openssh on 1 July 2024 for a pre-authentication flaw in OpenSSH itself (CVE-2024-6387). What changes is who answers for the joins between the parts, and on FreeBSD that is the project that built all of them.
Alpine is small, and that is its point. The minirootfs of Alpine 3.24.4 for x86_64 is 3,704,088 bytes, while the base.txz of FreeBSD 15.0 is about 165 MB. On FreeBSD the size is paid once per host: thin jails, in the Handbook's words, "share most of the base system" with the host, so a hundred jails do not carry a hundred copies.
Not every Alpine machine was exposed. A container that runs no sshd never was, and fixed packages for every branch back to 3.16 appeared the same day.
The bill
Today's default for a small Linux machine is an image assembled from projects that make reasonable assumptions about one another and test them separately. The bill went to whoever ran one of those images with password logins left at OpenSSH's default: read three changelogs, upgrade, restart the daemon, and then search /var/log/messages for an "Accepted password" from an account that has none.
On FreeBSD the bill came to nothing, because the default refused password logins and the library would not have answered with a star in any case.
A star in the password field has meant nobody since 1980, and on Alpine for ten years it also meant anybody with enough letters.