← Back to context

Comment by zarzavat

2 days ago

The real problem here is the C language. Our industry should have dumped it a long time ago and we’re now paying the price of prevarication.

Which of the several flaws in this bug chain would have been materially different with a language other than C?

  • Any use of sum types would have prevented this immediately.

    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 *.

    • This argument is weak. C, and specifically libc, has errno. The error code is there. Not in band.

      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:*").

      3 replies →

    • Nope.

      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.

      1 reply →

    • Even C has a better type system than C: All modern C compilers support an extension that lets you flag a function's return-value as requiring a null check.

  • C's lack of all of

    - 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.).

    • One of the small but important significances of Rust is that it very often gets to have the same machine code as a correct "in-band error signalling" strategy from C but with source code that the maintainers can actually understand.

      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...

      2 replies →

    • I remember mucking with returning multiple values as anonymous structs. It was tantalizing close but no banana.

      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);

  • For the other part of the issue (the truncation in SHA512), in Rust you could've had:

        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.

  • Wouldn't Rust's Result or Go's error checking prevent that?

    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

    • Rust and go still make it pretty easy to not check results if you don’t care about the success value (lots of code just does `let _ = foo()`), but if you do want the value (like with a hashing function), Rust makes it really hard to not check the error too. There’s no syntax to say “assume this worked and give me the success value, including whatever garbage is there if it’s not set”:

      - 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.

      3 replies →

I understand that other languages could help avoiding the semi-predicate problem. That being said, in C there are better ways around semi-predicates, as inelegant as they may be, is possble, is not an inherent problem of the language, but may a bad habit/idiom of it.