News for hackers. Stuff that matters.
Re:Sshole by BoppreH on Thursday November 05, 2026 @08:16AM (#50378718)
In-band signaling strikes again. * should not have been used as special value, neither for "no password" in /etc/passwd nor "error while hashing the password" in crypt. I'm afraid we'll find many more of these abuses, specially in string manipulations.
Re:Sshole by altcognito on Thursday November 05, 2026 @08:44AM (#50378975)
Some of us called them magic strings/values back in the day.
Re:Sshole by wolrah on Thursday November 05, 2026 @08:47AM (#50379004)
Also how many times do we have to learn that letting badly programmed software die when unexpected things happen is the better option because it means people actually notice that there's a problem. Silent compatibility hacks are the worst, they guarantee problems never get fixed and the stack of hacks never gets smaller.

Microsoft has historically been one of the leaders on the wrong side of this one, but they have also demonstrated how effective it is to call out badly behaved software and make it annoy the users with UAC. When Vista came out, so many applications needed elevated privileges to run because they were badly designed to write user data in system locations, but now their application was more annoying to use and came with a scary dialog about it making changes to your computer that other better-designed software didn't get.

More often than not if a program didn't actually intend to change system settings it was just a matter of badly chosen default paths which are pretty trivial to fix, but so many vendors just would not do it despite years of complaints from NT admins wanting to not have to spend hours with Process Explorer figuring out specific permissions to grant limited users enough access. Once their failures were put front and center, calling their application out as the one getting things wrong every time the user ran it, they suddenly had motivation to fix it.

Re:Sshole by simias on Thursday November 05, 2026 @09:02AM (#50379175)
For security it's better, but in many cases if you're not writing security-oriented software it can be worse from a user perspective. I remember the first job I had over 20 years ago was working on VoD streaming and one of the "tricks" to improve stability was mmap'ing the 0 page so that NULL dereferences wouldn't immediately segfault. Ugly as hell but when you were working with all sorts of more or less well coded media codecs and security wasn't a concern at all (worst case scenario the user would stream movies without paying) it made some amount of sense.

I suspect that Microsoft has the same history: the entire Windows platform was built with single-user PCs in mind, mostly only dealing with trusted input. The users were also expected to be tech-illiterate for the most part and unable to troubleshoot deep technical issues.

More recently I'm still working on video transmission, but on the production side this time. I also originally adopted the mentality that I should crash early if some unexpected situation was encountered, but I ended up walking that back a lot, because having a critical piece of live media production reboot mid FIFA worldcup final is generally Not Great. Degraded functionality is usually preferable. I still crash early in dev builds, but the production code just logs an error and continues if at all possible.

That said, I definitely agree with you for something like password handling. In this case, I do think it's warranted to crash the program without survivors if something is amiss.

Re:Sshole by stackghost on Thursday November 05, 2026 @09:47AM (#50379748)
> because having a critical piece of live media production reboot mid FIFA worldcup final is generally Not Great.

Story time?

Re:Sshole by simias on Thursday November 05, 2026 @04:52PM (#50384974)
There's not much else to say honestly, for these very important events there will usually be several layers of redundancy for the most critical feeds, so the production is always able to send something to the satellite uplink even while our devices reboot due to a crash, but brutally losing 90% of your inputs for ~1 minute while the machine reboots is not a pleasant experience for anyone...
Re:Sshole by pixl97 on Thursday November 05, 2026 @09:51AM (#50379798)
> because having a critical piece of live media production reboot mid

It's not great, but these days a trickster with AI will find some way to start broadcasting porn to a billion people because of the flaw, so may end up being the better option.

Re:Sshole by pmontra on Thursday November 05, 2026 @10:00AM (#50379927)
> if you're not writing security-oriented software

It's hard to define what's security oriented and what is not. For example, is this HN web app security oriented? Not in the way of sshd but it must keep our accounts data separated and it must not let people exfiltrate data from the db, where for example it is storing the emails of some of us.

Re:Sshole by funcDropShadow on Thursday November 05, 2026 @10:38AM (#50380466)
It depends on the time. Twenty or thirty years most software was not security-oriented. Nowadays, everything is online and therefore everything needs to be vigilant about security.
Re:Sshole by zicixisi on Thursday November 05, 2026 @09:17AM (#50379340)
Some applications, such as servers, often want to continue, even if a bug is detected in one part of the system, and the potential worst-case fallout from that specific kind of bug is limited. Though such bugs are often also logged, at the very least.

Different situations, different requirements, different options, different trade-offs.

Consider also the use cases in https://github.com/rust-lang/rust/issues/43596 , despite Rust defaulting to panicking. Also, despite Rust not having exceptions, some configurations allow for "catching" panics, meaning that some Rust programs have to be "unwind-safe", similar to C++ exception safety.

Re:Sshole by hnfong on Thursday November 05, 2026 @09:40AM (#50379648)
> Microsoft has historically been one of the leaders on the wrong side of this one

Objective-C ahem nil messaging...

Re:Sshole by sscaryterry on Thursday November 05, 2026 @09:46AM (#50379742)
Objective-C is an Apple thingie :)
Re:Sshole by usefulcat on Thursday November 05, 2026 @09:53AM (#50379821)
By way of NeXTSTEP
Re:Sshole by SoftTalker on Thursday November 05, 2026 @11:28AM (#50381134)
NeXT certainly used it extensively but they didn't invent it. Originally developed by Brad Cox and Tom Love in the early 1980s, per !w.
Re:Sshole by hnfong on Thursday November 05, 2026 @11:19AM (#50381004)
Yes and thus I dispute that Microsoft leads on this front :P
Re:Sshole by quotemstr on Thursday November 05, 2026 @09:57AM (#50379884)
> Also how many times do we have to learn that letting badly programmed software die when unexpected things happen is the better option

Sadly, we will never learn this because failing fast runs counter to intuition, and a class of person will never override it.

Re:Sshole by bulatb on Thursday November 05, 2026 @11:00AM (#50380798)
Moralizing leaves them trapped in epistemic closure.

They will look at problems caused by failing late and they'll see proof that failing fast is bad.

Re:Sshole by pizlonator on Thursday November 05, 2026 @10:11AM (#50380095)
> Also how many times do we have to learn that letting badly programmed software die when unexpected things happen is the better option because it means people actually notice that there's a problem.

100%

Re:Sshole by drdexebtjl on Thursday November 05, 2026 @10:38AM (#50380465)
[delayed]
Re:Sshole by zahlman on Thursday November 05, 2026 @10:47AM (#50380574)
Agreed. I feel like this attitude has been holding Python back, too. It's one thing to criticize "move fast and break things", but it's also not good to ignore already existing breakage just because the pieces still fit together neatly, and it's not good to refuse to move at all.
Re:Sshole by 0xbadcafebee on Thursday November 05, 2026 @12:57PM (#50382149)
[delayed]
Re:Sshole by dvdkon on Thursday November 05, 2026 @01:24PM (#50382526)
I'd still rather have the software crash and alert a human that to continue in a faulty state. The way to add robustness in this case is to add backup systems, not "ON ERROR RESUME NEXT".
Re:Sshole by 0xbadcafebee on Thursday November 05, 2026 @04:43PM (#50384859)
[delayed]
Re:Sshole by bottlepalm on Friday November 06, 2026 @12:14AM (#50388482)
> badly programmed software

Is there a test or check I can use to determine that software is 'good'?

Oh there isn't? Good/bad is subjective? We need to scape goating hacks as bad software, as if anyone is capable of perfect code. Perfect code would require knowing the system inside and out, the stack up and down - no one knows all that, no one is perfect. Sufficiently advanced AI though can rip through all of that, find the weakest spot and zero day it, bringing the entire house of cards down.

Software you thought was good can still be hacked. There is no way to create provably unhackable software.

Which means all software is packed with vulnerabilities just waiting to be discovered, no matter how good you think it is.

Re:Sshole by throw10920 on Thursday November 05, 2026 @08:52AM (#50379059)
This is a side-effect of Unix-y "it's all plain text" design that leans extremely heavily upon in-band signalling (among other design decisions).

If /etc/shadow was a JSON (or XML, even, although that would make me feel gross), this bug would have been much easier to see, because a field that reads '"hash": ""' sticks out a lot* more than a custom microformat that you don't understand.

With data structures, there's never "plain text" - it's all structured. The question is whether you want to use existing formats and tools, or create a new one for every tool and literally make footguns like this.

Re:Sshole by BoppreH on Thursday November 05, 2026 @08:59AM (#50379139)
Yes, absolutely. Take for example this cURL command:

  NO_PROXY=1 curl -qs --data @./my\ message.txt -- \
  "https://user:[email protected]/user/${userid}/my%20message.txt"
I count ten distinct separators and nine ways of escaping/tagging. We have seriously overused strings and we're now paying the price.
Re:Sshole by dolmen on Thursday November 05, 2026 @09:42AM (#50379680)
Also: free text is the input format of LLMs...
Re:Sshole by killerstorm on Thursday November 05, 2026 @09:09AM (#50379258)
I'd say another UNIX-y thing - "C is good enough" - is part of the problem. C makes programming painful which results in all sorts of hacks.

E.g. in Pascal you can can define a record and then defined file of record to have typed I/O. I.e. it comes with built-in serialization format supported by compiler. Not a good one - but perhaps a step in a good direction.

OTOH C people starving for serialization often believe that `struct` byte layout made by a specific version of a compiler is a serialization format - a view not endorsed by the standard.

Re:Sshole by adwn on Thursday November 05, 2026 @02:27PM (#50383290)
Pascal's record typed files have the exact same problems regarding endianness and word size as directly writing structs to files in C, so I don't see how they're any better. They're also a complete deadend and unusable for pretty much any file format from the last 40 years+, since you can't even handle a header or magic bytes at the start.
Re:Sshole by dns_snek on Thursday November 05, 2026 @09:20AM (#50379382)
[delayed]
Re:Sshole by BoppreH on Thursday November 05, 2026 @09:33AM (#50379556)
No, that's just more in-band signaling!

Some badly designed password hashing algorithms (cough bcrypt cough) have maximum password lengths, ignoring everything after X characters. So it could happen that I set my password to a 100-character string, it gets saved "==DANGEROUS_CRYPT_ERROR_length==", and then anybody can log in to my account by entering an unrelated long string.

If a function promises a string result, it should only provide a string if the function succeeded. The null was much better than magic strings.

Re:Sshole by dns_snek on Thursday November 05, 2026 @09:38AM (#50379626)
[delayed]
Re:Sshole by BoppreH on Thursday November 05, 2026 @10:01AM (#50379945)
If I was really desperate and could only return a valid string, I would probably use a message and add a random component to avoid ever matching two errors. Like SQL, where null != null.
Re:Sshole by knome on Thursday November 05, 2026 @10:11AM (#50380076)
>but the fact that crypt() doesn't have any way to signal failure

https://pubs.opengroup.org/onlinepubs/009604599/functions/cr...

>Upon successful completion, crypt() shall return a pointer to the encoded string. The first two characters of the returned value shall be those of the salt argument. Otherwise, it shall return a null pointer and set errno to indicate the error.

returning a sentinel value of NULL and setting errno is pretty standard C, and shouldn't cause an error.

OP's sshhole link specifically notes that it _should_ return NULL, but some MUSL devs decided they wanted it to return trash instead, and the trash they returned instead of NULL caused this.

All because some devs were too lazy to check the result and another dev decided to be clever and "fix" NULL errors from crypt by sending back 'unmatchable' trash banking on devs not checking NULL.

Re:Sshole by mafuy on Thursday November 05, 2026 @11:24AM (#50381084)
If this is true, then the entire thread branch here on HN is entirely wrong. What is going on??
Re:Sshole by hedora on Thursday November 05, 2026 @11:32AM (#50381165)
People don't RTFM, either on HN or when implementing "standards compliant" stuff like MUSL.

No error handling scheme can fix that problem. For instance, if a Rust API says I should return a Result<String, ENOSYS>, I can always write code like `if(ENOSYS) { return Ok("*") } instead of returning Err(ENOSYS).

Then my code would be compatible with MUSL.

Re:Sshole by dns_snek on Thursday November 05, 2026 @12:03PM (#50381534)
[delayed]
Re:Sshole by hedora on Thursday November 05, 2026 @01:16PM (#50382385)
They had to write

   if(ENOSYS) {
     return "*";
   }
after seeing that other implementations use different sentries like "*0" and that the spec says they MUST return NULL. That must have felt bad too.
Re:Sshole by dns_snek on Friday November 06, 2026 @04:12AM (#50389979)
[delayed]
Re:Sshole by zbentley on Thursday November 05, 2026 @12:42PM (#50381973)
Sure, but if you didn't do that, and returned the tagged union (in Rust or whatever language), your users would necessarily be forced to handle it in order to process the result.

The only reason this library did the insane thing of returning "*" is because it was extremely easy to cause crashes or false-positives (NULL == NULL) by returning NULL+errno. The reason that was easy is because C lacks a means of making error handling mandatory (exceptions, tuple returns, tagged unions, whatever).

If you return a tagged union, users might mis-handle it, it's true (https://blog.cloudflare.com/18-november-2025-outage/). But they have to handle it, which is a requirement that C APIs are incapable of providing.

Re:Sshole by hedora on Thursday November 05, 2026 @01:14PM (#50382353)
You have to handle NULL in C, or your program crashes (though I suppose a misguided optimization of strcmp could do "a == b || really_strcmp(a, b)", which gets us back to your bug).

Also, in things like rust you can just do result.unwrap() if you want. In general, language constructs don't really save you from bad programmers.

Re:Sshole by zbentley on Thursday November 05, 2026 @01:27PM (#50382566)
> a misguided optimization of strcmp could do "a == b || really_strcmp(a, b)",

In practice, that's less of a risk than:

    char* x = function_that_might_return_null_and_error();
    dangerous_function();
    if (x == NULL) {...}
Or code for which pointer identity is relevant (e.g. accessing slices/subparts of a data structure):

    char* x = returns_null_or_a_slice_pointer(data_structure);
    char* y = other_function_that_returns_null_or_a_slice_pointer(data_structure);
    if (x == y) { puts("you're fucked now!"); }
Re:Sshole by hedora on Friday November 06, 2026 @11:35AM (#50394640)
You can always write:

   x : Result<string, err> = function...
   dangerous_function();
   switch(x) { 
     Ok() ... 
     Error() ...
   }
There's no difference between result and null other than readability (there's a mechanical transformation between the two, where you use unwrap() in rust on an unchecked null dereference, and you read errno in C instead of looking inside the Err() result variant).

To be clear, I strongly prefer Result<> because of the readability, but any dumb thing you can write with NULL + errno can be written with Result (and vice versa).

Re:Sshole by zbentley on Friday November 06, 2026 @03:42PM (#50398217)
Totally! I threw that first example in because it's the poster child for things (checked) exceptions solve, and I didn't want to unfairly favor result unions (which I also prefer).
Re:Sshole by jstanley on Friday November 06, 2026 @03:29AM (#50389643)
> You have to handle NULL in C, or your program crashes

But it crashes at runtime, so such a bug can exist for years with nobody noticing.

The existence of such bugs is what motivated the musl developers to return "*" instead of NULL.

Re:Sshole by slashdave on Thursday November 05, 2026 @01:47PM (#50382813)
A lazy attitude like "if we just used <n> language, all our problems would be fixed".
Re:Sshole by fc417fc802 on Thursday November 05, 2026 @11:46PM (#50388334)
It's subtly wrong, just like most of the rest of the commentary in this comment section. Yes, crypt is specified to return NULL on failure but if you read the rest of that page it is only permitted to fail with ENOSYS. The spec does not allow for failure for any other reason.

So if you're strictly adhering to the spec in this case then unfortunately NULL is not an acceptable way to signal a generic error condition. Hence all the seemingly bizarre special values. (In a strange turn of events all those experts doing things that looked weird turned out to know what they were doing. Go figure.)

Re:Sshole by FLeXMurphy on Thursday November 05, 2026 @11:54AM (#50381432)
Except it's not trash. It's a backdoor funded by our glorious leader Kim Jong Un.
Re:Sshole by dns_snek on Thursday November 05, 2026 @11:57AM (#50381463)
[delayed]
Re:Sshole by zbentley on Thursday November 05, 2026 @12:36PM (#50381910)
This is true, and "trash" is putting it mildly.

But all those sentinels were added for the reason that, when your code is a public API intended to be widely used by who-knows-who, it needs to be really difficult to misuse it. NULL-plus-errno is easy to misuse. Yes, it's a dangerous mistake and your own fault if you do it. But people have been making that mistake for years and don't show any signs of stopping.

The root cause here is that C is fundamentally a rotten choice for public APIs of any kind. I griped more here: https://news.ycombinator.com/item?id=50381800

Re:Sshole by eqvinox on Thursday November 05, 2026 @02:27PM (#50383287)
Yup, that's the way to explain it in order to blame musl.

Here's how to explain it in order to blame openssh:

openssh processes passwords and doesn't want to reveal through timing whether a user has a valid password (or exists at all). So far so good.

What they do if they hit a user with no valid password (i.e. "*") is that they look for another user with a valid password, to use the seed from. A bit weird already, but OK from a perspective of avoiding implementation specifics on seeds and hash algorithms. They know at this point that the result must be to deny access even if the hashed value matches, because that has is for another user.

So they put the password received over ssh into crypt() with this other user's hashed value to get a seed. And then accept it if it matches, apparently?

Homework: what happens if the password actually matches another user? Can I log in as root if openssh randomly picks my user for the fake salt, and I type my own password correctly?

Re:Sshole by n_plus_1_acc on Thursday November 05, 2026 @05:28PM (#50385399)
The spec implies a string of at least two characters in the success case, am I reading that right?
Re:Sshole by panzi on Thursday November 05, 2026 @11:11AM (#50380920)
Personally I think there was nothing wrong with returning NULL. "But if people don't check for it it crashes their programs!" Good! Let it crash loudly! Then they notice and fix the issue.
Re:Sshole by hnfong on Thursday November 05, 2026 @11:27AM (#50381118)
> Also I think it could have returned ":". Given the file format of /etc/shadow and /etc/passwd I don't think it's possible to have a : as a value for anything.

I wonder if somebody else would have thought the exact same thing in a different (non-/etc/*) context, and then you get the same effect in a more obscure manner.

Re:Sshole by panzi on Thursday November 05, 2026 @09:54PM (#50387579)
Indeed. Let's keep NULL.
Re:Sshole by throw10920 on Thursday November 05, 2026 @09:30PM (#50387408)
> The issue isn't the config

That's a meaningless statement. There isn't a singular "the" issue like you said. Any one of several different technical decisions would have completely prevented the issue. Including, to my point, /etc/shadow being a JSON file where an account being locked is an `"active": false` field instead of an asterisk with no intrinsic meaning.

Re:Sshole by UqWBcuFx6NV4r on Thursday November 05, 2026 @09:28AM (#50379485)
At this point I’m resigning myself to think that we’ve just gotta wait for some people to die. It feels very dramatic and glib (no pun intended lol) and whatever to say, but at this point it feels like there’s a certain hostility in some parts to changing anything from the platonic ideals of however computing happened to work in 2004. “Oh we don’t need to mess with it!”
Re:Sshole by gspr on Thursday November 05, 2026 @09:34AM (#50379575)
I think this is a bit unfair. The way I see it, a lot of people are saying that changing breaks lots of old stuff, and risks complete fragmentation. Those are real costs to change. It might still be worth it, but let's not pretend that everyone is resisting change just because it's change.
Re:Sshole by abirch on Thursday November 05, 2026 @09:44AM (#50379707)
The costs to change are dropping significantly with AI.
Re:Sshole by gspr on Thursday November 05, 2026 @09:48AM (#50379766)
Citation needed.
Re:Sshole by Philpax on Thursday November 05, 2026 @10:47AM (#50380580)
Re:Sshole by gspr on Thursday November 05, 2026 @11:18AM (#50380985)
All I see is sausage makers advocating for a sausage-based diet.
Re:Sshole by xorcist on Thursday November 05, 2026 @12:29PM (#50381832)
How did you arrive at that conclusion? If anything, this vulnerability happened because someone decided to change the behaviour of crypt() without fully understanding the consequences. This specific vulnerability did not exist in 2004.
Re:Sshole by timhh on Thursday November 05, 2026 @01:55PM (#50382903)
I agree with him. I only managed to get asterisks enabled by default in sudo because sudo-rs isn't owned by people who have decided that the 70s was the perfect decade and nothing should ever change. And look at the insane pushback when it did!

I can't even get an obvious UX improvement through PAM because they like it broken.

https://github.com/linux-pam/linux-pam/issues/778

Re:Sshole by __MatrixMan__ on Thursday November 05, 2026 @09:54AM (#50379837)
A few years back I un-bashed my life as much as possible in favor of nushell (though it's not the only shell to encourage structure). I feel like it has really helped me notice a lot of this-needs-more-structure type problems outside of my shell that I otherwise might not have.

The elegance of the unix-y approach is actually that it's all bits. You can just join things together and shove the bits through, that's a wild simplification compared with having vendor-specific kinds of plugs.

But we could have that kind of compatibility while also benefiting from structure. The OS could probe either side of the pipe to see if they support negotiation. If not, send the bits through in the same old unixy way. If so, use the newest schema for structured data that is supported by both sides.

Re:Sshole by jonhohle on Thursday November 05, 2026 @10:24AM (#50380260)
At Amazon we had RecordStream which implemented most of the standard Unix tools on structured streams. It was pretty easy to write filters, aggregators, and formatted output in something not much more complicated than sed/awk/cut/join/sort pipelines. There was a convenient structured log format for services that was often used as input.

https://github.com/benbernard/RecordStream

Re:Sshole by hedora on Thursday November 05, 2026 @11:27AM (#50381121)
That looks really cool. I've been heading the opposite direction: posix shell for everything.

I have a bunch of management scripts, etc, that need to run on everything from a potato to a T-1000, and that's the only language that's always available. (OpenBSD sh, FreeBSD sh, busybox, bash, dash, zsh all can cope with it).

I'd love something that's at least JSON-typed (or equivalent), but that can be bootstrapped up on least-common-denominator posix.

Has anyone built something similar? (Yes, I'm being picky -- bun / ts is already a pretty low bar for cross platform support, but these scripts do stuff like install node, so I'm not totally crazy for asking.)

Re:Sshole by cowlevel on Thursday November 05, 2026 @10:09PM (#50387680)
Python?
Re:Sshole by hedora on Friday November 06, 2026 @11:37AM (#50394671)
Not installed universally, and it also doesn't reliably run python programs. (Minor + major semver changes break backwards compatibility, and pip mutates the global system state.)
Re:Sshole by raverbashing on Thursday November 05, 2026 @10:18AM (#50380182)
> "it's all plain text" design that leans extremely heavily upon in-band signalling

Honestly I've been shaking my fist at these since the XOn/XOff days

Re:Sshole by cwillu on Thursday November 05, 2026 @10:50AM (#50380639)
That's not specifically bash though, that's process exit codes
Re:Sshole by raverbashing on Thursday November 05, 2026 @12:55PM (#50382118)
Yes. But it's still contradictory
Re:Sshole by OnlineTux on Thursday November 05, 2026 @06:10PM (#50385878)
The question you are asking when querying a process exit code is "did this program exit with an error?". The default assumption is "no, it completed successfully", so 0 (false) is the logical expected default return.

A function is expected to produce a result - so the question is "what was the result?". !0 (something) is the logical expected default return.

I've never found it to be contradictory.

Re:Sshole by xigoi on Friday November 06, 2026 @04:08AM (#50389948)
But in shell, 0 is true, not false. The program called true returns 0.
Re:Sshole by Dylan16807 on Friday November 06, 2026 @06:16AM (#50390952)
"True has 0 errors" is the straightforward part.

"False pretends to have an error" is where it gets weird. It's not that they got booleans backwards, it's that they're stretching a tenuous analogy a bit too far.

Re:Sshole by raverbashing on Sunday November 08, 2026 @12:20PM (#50415175)
> The default assumption is "no, it completed successfully"

Correct

> so 0 (false) is the logical expected default return.

Nope

We came to expect zero because we got accustomed to it. And it is a "good enough solution" in memory constrained systems.

And note some programs attribute >0 and <0 to different types of errors

Re:Sshole by xorcist on Thursday November 05, 2026 @12:30PM (#50381845)
Error code 0 means success in a lot more places than just bash.
Re:Sshole by yearolinuxdsktp on Thursday November 05, 2026 @10:43AM (#50380535)
Returning “*” instead of NULL to signal failure is the real footgun. Don’t blame the format.

For historical reasons, we could’ve ended up JSON as “hash”: “*” just the same. The /etc/shadow format lets you skip values completely, like “::”, so your argument doesn’t hold.

Re:Sshole by hedora on Thursday November 05, 2026 @11:45AM (#50381323)
"panic" is a good thing. There's always some error condition that your code cannot handle. At some point, the least bad thing to do is crash.

For instance, what if your error handling code throws?

One spectacular example of panic saving the day was the Apollo 11 1202 error. It was basically a watchdog timer that fired if the landing computer was starved of CPU time. A misconfiguration caused the first moon landing to hit CPU overload. When that happened, the computer displayed 1202 and rebooted, then ran things in priority order. Apollo 11 landed in the middle of a tight 'panic, reboot, adjust thrusters' loop. Thanks to core memory, most of the heap survived the reboot, so the reboot step was fast enough for it to mostly land without human interaction. The pilot had to make ~1 adjustment to get it to actually hit the landing site, but the panic-ridden computer handled the rest of the descent.

https://dpma.de/english/our_office/publications/ingeniouswom...

Re:Sshole by zbentley on Thursday November 05, 2026 @12:52PM (#50382095)
sshole-type APIs < panicking APIs < required error handling.

Panic is fine, but fundamentally a whole-program crash is undesirable in a really wide range of use-cases. I don't want other SSH connections to crash (or the server to stop accepting valid logins until someone restarts sshd) if one person tries to log in with a credential that triggers a panicking bug in crypt(). Same for crashes in request handlers and so on.

The language/runtime itself doesn't (and probably cannot, at least if it's a general purpose language) distinguish between local and global error conditions, so panics (just like "throws" in Java or process boundaries in Erlang) end up being either unnecessarily damaging, or languages like Go end up normalizing panic "localization"/handling in dangerous ways.

The better solution is required error handling. If user code must engage with the possibility of an error state in order to access a result, that's a huge step forward. They can do the error handling wrong, but if you make most people have to do it at all most of the problems will go away.

"I forgot to check for an error" should not be implicated in bugs in 2026; we have had a large menu of possible alternatives (checked exceptions, unchecked exceptions, tagged unions, etc.) for decades. But we're still over here writing C (and Go), languages pathologically incapable of providing this ability.

Re:Sshole by hedora on Thursday November 05, 2026 @01:08PM (#50382275)
SSH specifically has separate processes per connection the last time I checked, so a panic does the right thing: The attacker's connection drops uncleanly.

This is also why you can upgrade SSH over SSH without having the connection drop mid-stream.

Re:Sshole by zbentley on Thursday November 05, 2026 @01:21PM (#50382463)
Sort of. It does some of the validation/auth code in the sshd parent daemon.

It has to: if it did 100% of the post-accept(2) work in a subprocess it would be pretty easy to remotely forkbomb/PID-exhaust a system by spamming invalid logins.

> You cannot avoid "I forgot to check for an error"

Yes you can. Checked exceptions and tagged unions both force calling code to be written with error-awareness.

In SSH's case, "I forgot to check" might lead to crashes (which might knock over sshd so new connections are impossible), or it might lead to false-positives via comparing NULL == NULL.

I'd much rather that boneheaded API users have to write code like

    resultstruct x = authenticate_user(user)
    if x.haserror {
        rv = { .user = user, .result = "successfully authenticated" } 
    } else {
        rv = { .user = user, .result = x.result }
    }
    return rv
At least that way some keystrokes are required to do the stupid thing. People still will (https://www.schneier.com/blog/archives/2008/05/random_number...), but the harder we can make it, the better.
Re:Sshole by hedora on Friday November 06, 2026 @11:32AM (#50394586)
How do you handle out of stack space on 'authenticate_user'? What about the machine check exception loading the DRAM page that contains "successfully authenticated"?
Re:Sshole by zbentley on Tuesday November 10, 2026 @08:56AM (#50440419)
Dealer’s choice. Reallocating stack return values (ideally with compiler support) and aborting on stack overflow isn’t a bad combination, since you get decent guarantees about whether a function has enough memory to run until either completion or the next function call. POSIX’s out-of-stack-space behavior isn’t terrible here either.

As for DRAM: if by “exception loading page” you mean an OOM, then have malloc use the same error reporting system. If you instead mean “some other failure faulting/fetching a page from main memory”, well … crash? I don’t know of any platform that has semantics for handling that condition. Hardened spacecraft OSes, maybe?

Re:Sshole by throw10920 on Thursday November 05, 2026 @11:17PM (#50388119)
> is the real footgun

This contains a false snuck premise - that one and only one thing was the footgun. This is categorically not true. An obtuse, poorly-understood, purpose-built text-based DSL is absolutely a footgun and if it was replaced with a JSON and inactive accounts had been indicated by `"active": false` instead of a freaking asterisk then this would absolutely would not have happened.

> For historical reasons, we could’ve ended up JSON as “hash”: “” just the same.

That's dishonest, because it intentionally conceals the fact that that's a vanishingly* unlikely scenario.

People have brains that, factually, are far more likely to see a single context-less character "" and not realize the implications of it than to see `"hash": ""` and think "oh, that's normal".

This is just straight-up psychologically true.

> The /etc/shadow format lets you skip values completely, like “::”

Totally irrelevant.

> so your argument doesn’t hold.

False `::` is both (1) still harder to understand than `"active": false` and (2) even though the syntax existed it wasn't used here because everyone thought they understood the system.

Don't make excuses for this insane text syntax. It is, factually, harder to understand than a decently-written JSON format.

Re:Sshole by __MatrixMan__ on Thursday November 05, 2026 @09:35AM (#50379589)
PSA: you can typically declare different types of string so that your type system objects to mixing data of different types even though deep down we know they're both strings.
Re:Sshole by BoppreH on Thursday November 05, 2026 @10:28AM (#50380315)
If you mean type aliases, that's a good idea within a single codebase, but the distinction between different types of strings doesn't survive serialization so it wouldn't have prevented this error. They really needed to return either null or a structure that includes a tag indicating success or failure (aka Result<T>).
Re:Sshole by __MatrixMan__ on Thursday November 05, 2026 @10:34AM (#50380427)
At least in python there's a difference between type aliases and typing.NewType (the difference described here: https://gist.github.com/simonwhitaker/3a24d669b26b4f373690c6...)

That's what I was thinking of. I assume something similar exists in other languages.

Re:Sshole by __MatrixMan__ on Thursday November 05, 2026 @11:07AM (#50380881)
In python, aliases are separate from typing.NewType, but you're right: it's only enforced within a codebase and then only if the dev cares to run the typechecker.

So yeah, I guess it either needs to happen at runtime, or we just do signaling and user data on separate file descriptors.

Re:Sshole by Normal_gaussian on Thursday November 05, 2026 @11:47AM (#50381343)
This is presuming that the type system is nomitive rather than structural and there is not serialisation/deserialisation or type refinement occurring. A structural difference is much more preferable.
Re:Sshole by mkl on Thursday November 05, 2026 @09:55AM (#50379842)
Even using "no_password" and "error_while_hashing" would have prevented it. The problem was picking a generic, meaningless signal string.
Re:Sshole by mort96 on Thursday November 05, 2026 @10:15AM (#50380133)
That would've happened to fix it, because "error_while_hashing" and "no_password" happen to not be valid password hashes. The right way would've been to have different syntax for hashes and non-hashes; you could've made it so that a hash starts with a '#' and if it doesn't start with a '#', the rest of the string should be ignored.

Or, for backwards compatibility, start non-hash strings with a character which can't appear in a hash, such as '@'. Your examples would then become '@no_password' and '@error_while_hashing'.

Re:Sshole by SoftTalker on Thursday November 05, 2026 @11:26AM (#50381105)
Well neither * nor ! (the two characters I've seen to indicate "no password") will appear in a valid hash. The problem was using a character that could also be returned as an error indicator, IIUC.
Re:Sshole by mort96 on Thursday November 05, 2026 @02:10PM (#50383087)
Yeah but it's not forwards compatible with future hashes where '*' is suddenly a legal hash, which is what happened here. You need a grammar which lets you tell the difference at the parsing stage between "this is a password hash" and "this account has no password". That's not the same as letting the value '*' flow through the system as normal and hope it doesn't happen to match the hash.
Re:Sshole by SoftTalker on Thursday November 05, 2026 @05:57PM (#50385735)
True, but if they would have picked ?, or #, or any character other than * with its designated special meaning in the /etc/shadow file, then we would not be in this pickle.
Re:Sshole by cowlevel on Thursday November 05, 2026 @10:14PM (#50387725)
hashes can't start with * and there's no reason to think they ever will
Re:Sshole by mort96 on Friday November 06, 2026 @08:46PM (#50400836)
Well, turns out that they sometimes do
Re:Sshole by deniska on Thursday November 05, 2026 @01:11PM (#50382315)
Technically they did use a character which can't appear in a hash, '*'.
Re:Sshole by mort96 on Thursday November 05, 2026 @01:36PM (#50382691)
Right but not in a "rigorous" way. They kept the format of the file the same: the string between the colon characters is a hash. They exploited the fact that an asterisk happens to never appear in a hash, and just let the system believe the asterisk was supposed to be a hash to be matched again.

In a sense, they should've changed the parsing logic to return a struct with a field 'password_hash: Option<String>', and defined the grammar of the field to be: "a hash string, or '*' for no login". The bug is, in part, caused by the haphazard way they don't change any formal grammar or contract and just relied on the fact that '' happens to* never be a valid hash.

It's the difference between "happens to work" and "defined to work by construction". The former approach breaks once a hash function is invented where '*' is a legal hash (such as this new musl crypt() hash function where all strings longer than 256 bytes hash to '*'). The latter keeps working regardless because the asterisk is treated as "no password" already at the parser stage.

Re:Sshole by f33d5173 on Friday November 06, 2026 @03:40PM (#50398189)
There were different syntaxes for hashes and non hashes. The problem was that both a non hash "this password isn't set" and a non hash "this call to crypt produced an error" used the same non password value.
Re:Sshole by hedora on Thursday November 05, 2026 @11:15AM (#50380958)
Exceptions are almost never the right choice. They create extremely complicated control flow extremely quickly, and then you have to test all the different control flows. Java software is an example of how this goes sideways fast. There's no way to create a class that prevents callers from reading the object before the constructor runs (in multi-threaded software, new first hands a reference to the caller, which can put it on the heap, then it calls the constructor), and objects are default initialized, so all fields (even static ones that are assigned in the constructor) can be null.

So, any field de-reference in Java can produce a null pointer exception. Try testing all paths in something like:

try { foo.sideEffect1().sideEffect2()...sideEffectN(); } catch { bar.sideEffect1().sideEffect2()...sideEffectM(); } finally { quuz.sideEffect1().sideEffect2()...sideEffectP(); }

There are M * N * P paths. The only way to write testable code with exceptions is to panic on uncaught exception, and to never catch unexpected exceptions. Even then, you're relying on nothing unexpectedly throwing the exception type you caught.

I'd argue Result<T> >> NULL >> sentries >> exceptions for this reason. Sentries are terrible, but at least they're not computationally hard to use correctly.

Re:Sshole by BoppreH on Thursday November 05, 2026 @11:51AM (#50381403)
I'd argue that's the fault of having default nullability, not the exceptions. You would have the same problem if every attribute access returned a Result<T> too.

And I'm not a fan of breaking control flow, but I still find it preferable to nulls. A null may travel far from the place where it was generated before blowing up, making troubleshooting harder, or it might come from a function you called for its side effect but forgot to check the return value. I gotta say I'm also a careful enthusiast of checked exceptions, it's great knowing which ways a function can fail.

But I think the consensus is growing that Result<T> (plus warnings for unused return values) is simply the superior choice.

Re:Sshole by hedora on Thursday November 05, 2026 @01:21PM (#50382460)
C has what you are asking for, mostly:

   int foo f() __attribute__((warn_unused_result))
As long as an error condition on the return value causes a crash on use, then it's mostly the same as result. (The main remaining difference being that you do an implicit `.unwrap()` in C, and in things like rust that's an explicit code smell.)
Re:Sshole by Sohcahtoa82 on Thursday November 05, 2026 @01:23PM (#50382506)
> There's no way to create a class that prevents callers from reading the object before the constructor runs (in multi-threaded software, new first hands a reference to the caller, which can put it on the heap, then it calls the constructor)

Which is why the pattern of double-checked locking doesn't work.

https://en.wikipedia.org/wiki/Double-checked_locking

Re:Sshole by cowlevel on Thursday November 05, 2026 @10:08PM (#50387676)
It's wrong. The caller doesn't get the reference until the constructor runs. Although it looks like two operations in bytecode, the only thing you can legally do with an uninitialised object reference is call its constructor.
Re:Sshole by hedora on Friday November 06, 2026 @11:45AM (#50394767)
In java, the caller gets the reference before the constructor runs. It's in the language spec. It's terrible. You can prove it yourself with something like:

   this.dangerous = new Lol();
and then have Lol()'s constructor sleep for 10 sec then set done = true at the end of the contstructor. In another thread:

   while(!this.dangerous) { }
   assert(this.done) // will throw.
I guess you could use double-checked locking as a class author by having every public method on your class do a if (!this.done) { throw; }, and make sure there are no public fields.

That's pretty lame though.

Re:Sshole by cowlevel on Friday November 06, 2026 @09:59PM (#50401215)
That's a data race. The caller gets the reference after the constructor runs, but writes to non-final fields from the constructor might not be visible to other threads.
Re:Sshole by hamilyon2 on Thursday November 05, 2026 @01:27PM (#50382562)
> never catch unexpected exceptions

Yes, broad exception catch clauses are simply incorrect code.

Panic is maybe an overreaction, "scratch everything and orderly abort" is sufficient.

Re:Sshole by Ey7NFZ3P0nzAe on Thursday November 05, 2026 @01:32PM (#50382629)
I'm inclined to say a Panic is absolutely preferable to root auth.
Re:Sshole by gf000 on Thursday November 05, 2026 @02:38PM (#50383436)
> There's no way to create a class that prevents callers from reading the object before the constructor runs

That can only happen if you leak the reference (given we are talking about java language level semantics and not byte code), which I give you is unfortunately not that hard to do.

As for exceptions, I think they would pair beautifully with software transactional memory, which would work well with effects. In that case (and no side effects) it would not matter what code path we took, we could just go back to a known safe point.

Because ergonomy-wise I still think exceptions win (even though I do like Maybe types, and Rust's ? macro is plenty good): - they let one focus on the actual happy path code - no error is ever swallowed, unless explicitly written so (looking at you go with useless if errs that fail to properly handle the error), they are always handled somewhere - they automatically "unwrap" the Ok result, and automatically bubble up the Err side. - bubbling up is especially useful because most errors can't be handled at their source (e.g. IO error is meaningless, how you handle it depends on what you want to do in the first place, which is done several layers higher) - they automatically attach a stackframe/source, while with result types you may get a list of Ok Ok Ok Err and have no idea how that Err got there.

Re:Sshole by hedora on Friday November 06, 2026 @11:41AM (#50394735)
As the author of the class, I cannot prevent my caller from leaking the reference, so my public API allows for pre-constructor invocations of all my methods.

Software transactional memory doesn't really work with exceptions, since it precludes the use of catch + finally. It looks more like a top-level panic handler (where the throwing code cannot permute the heap, which is cool). To get exceptions, you'd need nested software transactional memory, which gets into obscure transaction processing theory. There's also the problem that your transaction could side-effect the outside world, then throw and be aborted, so you need to rework how I/O works if you walk down that path.

Exceptions are basically write-only, so I agree that their ergonomics for that use case are pretty good. Panic is better though, since it's really no error handling. Panic is easier to test, and easier to read / understand.

Re:Sshole by quotemstr on Thursday November 05, 2026 @04:52PM (#50384984)
Exceptions work fine and enable a clean separation of business logic and error handling. Visibility into partially-initialized objects is a language design issue independent of exceptions. In a non-exceptional language, the corresponding access to uninitialized fields would just be bugs, not errors you can see.

Nothing in your post actually supports the idea that exceptions are "computationally hard". The CFG error edges exist however the errors are spelled

Re:Sshole by hedora on Friday November 06, 2026 @11:48AM (#50394821)
It is difficult to get to M * N * P control flow edges with O(1) result unwrapping code.
Re:Sshole by quotemstr on Friday November 06, 2026 @12:42PM (#50395615)
You have exactly as many edges if every fallible routine can throw and every infallible one is marked as nothrow.
Re:Sshole by z3t4 on Friday November 06, 2026 @04:23AM (#50390089)
[delayed]
Re:Sshole by za3faran on Friday November 06, 2026 @05:50PM (#50399570)
> There's no way to create a class that prevents callers from reading the object before the constructor runs

Doesn't this address the issue? https://openjdk.org/jeps/539

Re:Sshole by jedbrooke on Thursday November 05, 2026 @02:04PM (#50383013)
Unfortunately we’re moving to llm based systems which rely entirely on in-band signaling.
Re:Sshole by sepeth on Thursday November 05, 2026 @06:20PM (#50385967)
It also looked to me a bit like a semipredicate problem:

https://en.wikipedia.org/wiki/Semipredicate_problem

Re:Sshole by hobo123 on Friday November 06, 2026 @07:48AM (#50391712)
Should have set crypt_errno.