I think for any one of those bugs there might simply be half a dozen footguns or bugs that once it's spelled out you wonder how anyone let it slide.
I think that's the case where LLMs might indentify most.
It's challenging though as the current AI audits produce so many false positives it's hard to know when it's being pedantic or when it's genuinely caught a human being a bone head
It is not really that hard to find in the wild if you had a device built this way, when overflowing buffers on embedded devices is a standard attack vector.
It might be hard to root cause, but it would not be that hard to find.
Each one relies on a perfectly valid argument - that * can't match because we all know it'll be rejected by the other thing.
But that's not a perfectly valid argument, and it never was.
While not mandatory, a person would probably use it instead of the * hack.
Simply because the calling side wouldn't have an option to forget the check like with the NULL case
- result.unwrap() will panic if it failed
- result.ok() throws away the error but gives you a None value instead (still can’t use it as if it’s a success)
- matching on the Ok variant and doing nothing on the Err variant, you only get to use the success value inside the Ok arm
Etc etc.
The problem is when the compiler says “NULL is a valid instance of T”, which go lets you do, and rust doesn’t (without using the unsafe keyword and intentionally casting a null pointer, which you don’t do by accident.) Go lets you write code like `var hash string; hash, _ = hashfn()` and it compiles and now you have null instead of a valid hash.
This by the way is Tony Hoare's famous "Billion Dollar Mistake"
The whole thing exists because APIs that return a NULL value that "nobody checks", plus a string that you shouldn't use if you actually did check the null value.
In languages with sum types, this cannot happen.
Either you get your hash, or no result at all. You couldn't get eg NULL and *.
the fault for sshhole isn't just "C bad", it's a series of poor choices in workarounds falling together to create a vulnerability.
being condescending about decisions made 50 years ago, before any of the better languages you're mad the developer didn't use even existed, is silly.
if you want to lay blame, lay it at the feet of more recent devs that started returning values instead of NULL because people were using crypt incorrectly. likely sha512 returned a fake value instead of a real one because prior hashes couldn't fail. the interface should have been updated then, but instead they worked around it.
I can think of two common patterns that I've seen frequently:
* Return NULL for errors (sometimes with a global errorno() function).
* Return an integer error with any return values as out parameters.
So if it is possible to improve, why doesn't anyone do it?
> * Return an integer error with any return values as out parameters.
You must first standarize all that (usually negative) integers, make docs, check for it, do not make mistakes in your if's or case's...
And errno is just that - integers with a meaning. errno is even thread local now and quite safe to use but people on internet scream loudly when someone try to use that not app global varible :) I guess their llms statistically have less new texts feeded in ;)
At some point it is just better to return pointer to struct with all info you can imagine.
And how you think other languages implemented their Error type returns ? Just a struct or union with tag. Because current CPU's operate on simple type registers there is no other sane way to do it.
Because in fact all the changes were about changing the function signatures. The function signature is enforced by the linker. The semantic of strings that go through isn't.
Returning 2 values is possible in C (via pointers input parameters). The main issue here is that libc is a strong API contract based on C function signatures, that nobody can change, only augment with new symbols. Such strong API compatibility constraints happen in any programming language, and workaround to fit new behaviours in old APIs happens everywhere.
But also, its not like there were many alternative languages when the POSIX standards were designed so it's fair that it works like this.
But there has to be something in the payload field when it is not applicable. A null is perfectly fine.
The argument for changing that to "*", if I understand the above linked discussion correctly, was that some users of this api threw away errors and a dummy value would be harder to ignore. Maybe one could argue that these behaviours are less common in Rust or Go, but not because of a technical difference.
The world is not perfect and sometimes you really need to work around bad users. Even in Rust or Go. I am not convinced this was such a case. If you are convinced it was it would still have been better to select a less trivial dummy value (such as "*:ERROR:*").
Not in languages with ergonomic sum types. A `Result` or `Either` will have no payload field on the error type. The compiler won't let you dereference it.
Maybe
——————
Just hash
Nothing
Either
——————————
Left err
Right hash
The program would be designed around not allowing this failure mode to even be possible.- sum/union types
- multiple return values/tuple types
- exceptions
means that most ways of returning an error from a function are not available in C. Because of this, many C APIs, including `crypt` in this case, resort to in-band error signalling by using a sentinel value (like NULL, a negative integer, "*" etc.).
Most other languages lack one or two of the above, but very few lack all three, and so most have developed a different culture around error reporting (Result in Rust, multiple return values in Go, exceptions in Java, Python etc.).
However, I also think "weakly typed" is not a well-defined concept, everyone seems to apply it differently. In particular, it seems to mostly mean "playing it loose with types in a way that I don't like".
Implicit conversion could be seen as weak typing, but it is mostly limited to specific arithmetic operations in C. I would see implicit conversions as problematic if they allowed operations that make no sense. Decay of arrays and function to pointer conversion also do not seem problematic to me for this reason.
Void pointers are also weakly typed, but they are specifically designed for this. So if you do not want this behavior, one would simply not use them. It is a bit like using "unsafe" in Rust.
There are certainly stronger typed languages, but on the full range C is more on the strongly than weakly typed side in my opinion. A language that allows arbitrary implicit conversions between pointers, would be weakly typed, or which allows calling functions with wrongly typed arguments (which was true in the past for C a long time ago)
And it's very evident why the difference matters. People gravitate to what's readily available and ergonomic. That's why every C API under the sun (most definitely including libc itself) does janky stuff like returning -1 or "*" on error instead of building their own bespoke Result-style abstraction. A programming language is not just the things that are in some way possible within it, but more importantly the things that are easy in it.
A language should encourage writing robust/secure code, not just make it possible.
In any case, I do not think the issue here is caused by not having a pre-defined result type. Checking for null would have been fine. The issue was that to accommodate old and broken code a compromise was made. But as long such compromises are done even where fixing such old code would be quite easy, it is clear that changing to another language is completely unrealistic. So what are we even talking about? The real issue is that nobody wants to spend money or effort maintaining the old code. No language can fix this.
And really often when your "in-band error signalling" in Rust is more expensive you realise (perhaps to your horror) that your C wouldn't be correct, so you just dodged a bullet. For example Option<OwnedFd> is, like OwnedFd, a 32-bit value, Unix has always reserved -1 to mean invalid, so Rust can treat that as a niche. But the Windows equivalent Option<OwnedHandle> can't do this because, well, cue Raymond Chen: https://devblogs.microsoft.com/oldnewthing/20040302-00/?p=40...
But aside from NonZero- and NonNull-based layout optimizations, I don't think Rust is doing anything special to obtain that machine code - it's just relying on LLVM to optimize things.
But sure, for the most part LLVM will do The Right Thing™ here without much help and Rust is just giving better ergonomics to the same backend.
I think with C23 you got half a banana now. Still can't do it because C23 didn't adopt proposals to allow returns of untagged/anonymous structs.
If you could then you could write
auto rtn = Foo();
if(rtn.err)
printf("err=%i", rtn.err);
else
printf("Foo=%i\n", rtn.foo); struct Input<'a>(&'a [u8]);
impl<'a> Input<'a> {
fn new(input: &'a [u8]) -> Result<Self, Error> {
if input.len() >= 256 {
return Err(Errro...);
}
Ok(Self(input))
}
}
Now the truncation is a terrible idea in the first place, but at least with this it would have been entirely obvious to the caller that it was there.In any case, you can not change the API of an existing function. But returning NULL for error has required by POSIX would have been just fine.
Granted I mostly work with high level programming languages so I may be missing something.
That being said, OpenSSH did mismanage their attempts at handling a garbage return-value from crypt(), so there's definitely some blame to go around.
When later it turned out that musl's procrustian stack size made Python not really runnable under it, it cemented my resolve to not go near musl.
And now we have this.
And why because some dude was complaining sshd or whatever was crashing because it didn't handle nulls? Just handle the null my guy you already know why your bullshit is crashing.