Hacker News new | ask | show | jobs
by nolok 960 days ago
In memory safety ? Yes, the language is much better at being safe by default. But it does nothing for logics bugs.

The thing is, replacing from C (sudo or anything else), the number of exploit due to null pointer or buffer abuse or ... represent easily 50% of it.

4 comments

"But it does nothing for logics bugs."

"Nothing" is too strong. It does not solve logic bugs, but type systems stronger than C can solve some logic bugs too.

Even something as simple as having some concept of "private" and "public" and some boundaries between them can help. I'm writing some code right now in Go, hardly a super strong type system, but I've still put some basic barriers in place like, you can have a read-only view of the global state, but the only way to write to it is to a per-user view of that state, and the only way to witness the changes to the underlying value is through one of those per-user write handles. This eliminates a large class of logic errors in which one accidentally reads the original global state when you should be using the per-user modified state or vice versa. This is a rewrite of some older code, and this error is so rampant in that code as to be almost invisible and probably in practice unfixable in the original code. (Which was solved in practice by only every dealing with one user at a time, and if there was multiple users, it simply ran the process completely from scratch once per user. It tried to cache its way out of repetition of the most expensive stuff, but, the cache keys had some of the same conceptual underlying problems, so it just wasn't as good as it should be.)

You can't solve everything this way. Rust's stronger type system offers more options, but you can't solve everything with that either. But with good use of types, there are still classes of mistakes you can eliminate, and classes of other mistakes you can inhibit.

(There are some tradeoffs, though; with bad types you can alse mandate incorrect usage. But I think in the case of something like a sudo replacement we can reasonably assume fairly high skill developers and that there will be a lot of high skill oversight, as evidenced by the fact they've already sought out a third-party security review.)

C does have some notion of visibility: put private declarations into the .c file instead of the .h file and declare static linkage. You could have a function that returns a pointer to const for read only data. Obviously they can cast that away, but other languages have unsafe escape hatches too. C also has static analyzers to help with some classes of bugs.

Cowboy code might be common, but you don't have to do that. If using something C-like, C++ definitely gives you a lot more tools to write safe code (or hang yourself, up to you) though.

C has "some notion" of a lot of things. That doesn't make them particularly usable at scale. C has the worst static typing of a language that can even plausibly call itself statically typed in the modern world.

C++ is an option to obtain the sort of thing I talked about, yeah, but in 2023 you need to use something memory safe for something as important as sudo, and C++ on its own is not. C++ and a great static analysis tool would be the minimum I would consider acceptable, but there is something to be said for things like Rust that build the analysis all the way in to the compiler rather than relying on external tools, and then future Rust external tools can build on that even more solid foundation if even more assurance is needed.

Enums, Option and Result types, absence of null, not to mention that the type system, borrow checker, and static everything by default, rewards encoding application state and state transitions using all these mechanics, such that they can be verified at compile time. I'd say the language does quite a lot to address logic bugs as well as memory safety. It can't protect a determined developer from themselves, but it provides incredibly useful tools to anyone who can work out how to use them.
Even if I don't like the design of Rust's borrow checker I still do appreciate how Enums/Option/Result types and pattern matching can make your code more robust. Really wish I can bring some of them to C++... I frequently use a poor-man's version of Result types with a `TRY()` preprocessor macro, but I'm often jealous of what Rust has in its toolbelt.
Isn't Rust's result type basically the same as Abseil's Status, or am I missing something ? https://abseil.io/docs/cpp/guides/status
Generally the same idea, yes. Your parent mentioned a key difference though: "and pattern matching." enums in Rust have much stronger language support.

But there are also differences, for example, errors must be absl::StatusCode, whereas enums in Rust allow for arbitrary error payloads.

Also don't discount ecosystem usage: everyone uses Result in Rust, abeseil isn't used by most things, and std::expected has its own issues (though I can appreciate how tough making those calls is) and only landed in C++23, so it's not as widely used as Result either.

Sibling comment mentioned pattern matching, but didn’t point out the important point that the rustc compiler makes sure all patterns matches are exhaustive.

To use a C example, if you add a new definition/variant to an enum, suddenly all switch statements over that enum will fail to compile (unless there is a default: branch).

This does eliminate a large swatch of logic errors, though by no means all.

Static-everything is such a gimmick in my opinion. It sounds great until you try to do something useful with your code. It's almost never the case that people actually want to hard-code stuff in the source code.

Almost always you read configuration files at run-time (like sudo does) and change your behavior depending on run-time information - so you will have run-time errors.

“Static” here means that variables are const by default, and you can’t modify one without explicitly marking it as mutable.

In your case, a config object would be mutable inside the function that loads it from disk into memory, then read-only everywhere else by default.

I use rust, and it does have static by default in many places (for example it's hard to do the traditional OOP virtual polymorphism or to keep objects of various types in one container) and it makes it pretty hard for me to write "nice" looking code.

It usually devolves into a lot of nested if-else and switch (match) instructions.

I haven't run into that so much myself. What I have run into is trying to write C-but-in-Rust, for which the compiler yells at me to please knock it off. It got way easier when I gave up and committed to doing things the Rust way.

Not saying you haven't done that, just sharing my personal experience with it.

One of the habits I struggled to get rid of at first was the habit of making lots of things in structs refs for no reason, where it would be typical kn C for C reasons.

This led to a lot of unnecessary friction with the borrow checker while I was still getting to understand it.

Once I started always asking myself "does this really need to be a ref?", things became much easier. And this in turn revealed itself as a useful rule of thumb for keeping coupling between subsystem in check.

Then I was doing some C work and I realised I was asking the same question to myself about pointers too. And I found it could often reduce cognitive load down the line, because I'd end up in much fewer situations where I'd have to figure out "who should free this, and when?".

That's when I realised this cognitive load of keeping the borrow-checker happy was always there in C too. It was just more diffuse, and invisible until it blew up in my face. And when it did it was in the form in nasty runtime problems, not a helpful compiler error that happened before the code was even checked in.

If your program doesn't have a way of reloading its configuration at runtime, then even that first object created by reading the configuration from file can be immutable.
Yep! What I mean, though, is that the loading function itself will need to mutate the object as it reads settings from disk and updates the in-memory data structure. Once that's done, you can pass that around as a read-only object.
This gets said a lot, but I am coming to believe that the case is overstated. For two reasons:

1. Valgrind exists. It's not perfect, but it does arguably do a pretty good job as long as you're writing modern C. The biggest gap I'm aware of is that it can't really help you with global pre-allocated buffers. But I don't think that any language or tool can effectively protect you from information leakage if you're doing that sort of thing, not even Rust.

2. Memory-safe is not the same thing as secure. Programs written in memory-safe languages are rotten with security vulnerabilities, too. Rust's happening to be a memory-safe language that doesn't use garbage collection does not render it immune to this situation. It has some protections around concurrent usage of data that do add additional safety under certain circumstances (assuming you don't switch them off), but I doubt it's a panacea. I worry, though, that the Rust community's tendency to pitch this stuff as a security panacea could breed a culture of complacency that negates the advantages that Rust does bring to the table for systems programming languages. People tend to take unnecessary risks when they believe they're invincible.

> Valgrind exists

You may be right on an infinite frictionless plane, but unfortunately that does not work in real life, cf. e.g. https://msrc.microsoft.com/blog/2019/07/why-rust-for-safe-sy...

> Memory-safe is not the same thing as secure.

And safety belts do not help you if your car is on fire, still it's better to wear it.

The fundamental problem with valgrind is it only looks at what happened, not what could happen. Valgrind is great at making sure you don't have memory safety issues for "normal" inputs, but is basically useless at making sure your code doesn't have memory safety vulnerabilities when fed atypical inputs.
It's true that it doesn't eliminate all bugs in general, but it can completely eliminate buffer overflows for example.

There is no excuse to not at least have bounds checking. This is one of the most basic memory safety problems and it's trivial to prevent.

Just preventing this small issue will prevent a non-trivial fraction of bugs. I don't have sudo's bug list on hand but I wouldn't be surprised if 25% or more are caused by buffer overflows.

So even if it doesn't prevent all logic bugs, it cuts out a pretty big chunk of the bug list.

>assuming you don't switch them off

You can't switch them off.

>Rust community's tendency to pitch this stuff as a security panacea

I've not seen anyone claim this so far.

I’d rather have safety default on with an opt-out, rather than the inverse that C gives you with -Werror -Wall -Weverything -Wyesireallymeanteverything. Compile it again one two different architectures, compile yet another time with clang-tidy and then static analysis with Coverity just to be sure. Run it with valgrind, asan and thread sanitizer. Sprinkle some fuzz testing on top.

Yet you still don’t the same level of confidence as a rust program that may have a small unsafe block in one corner of the code.

From this link:

>It’s important to understand that unsafe doesn’t turn off the borrow checker or disable any other of Rust’s safety checks: if you use a reference in unsafe code, it will still be checked.

Unsafe rust basically just lets you use raw pointers, mutate static variables, use C-style unions, and do FFI calls, but otherwise it's exactly the same, and the safety checks are not in any way disabled.

The main thing is that pointers let you access whatever memory you want, and borrow checking the pointer value itself doesn't prevent this.

I don't think I would describe this as "switching them off", I would describe it as, "using raw pointers" or something along those lines.

I feel pretty good about the fire safety measures at my apartment despite the fact that I own several lighters.
Even unsafe Rust comes with significantly more checks and safety built-in than C.
Many vulnerabilities rely on crafting very particular inputs that trigger memory corruption in programs. Unless you happen to have fed that same input to your program when running it under Valgrind then Valgrind is useless for this case.
> It's not perfect, but it does arguably do a pretty good job as long as you're writing modern C.

There's no such thing as modern C. C code that's written neatly and meticulously looks the same today as it did 30 or 40 years ago, except for language changes such as the move from K&R declarations to function prototypes. C the language hasn't changed since 1989 except for minor things like mixed functions and declarations, the introduction of the long long type, restrict pointers, designated initializers, compound literals, and threads being in the standard library.

Is that because theyre easy to find, or because theyre the worst?
A lot of the most serious security vulnerabilities are memory safety because e.g. remote code execution is very often along the lines of "LOL, I smash buffer with machine code, it gets executed" and that's a memory safety problem.

For sudo you have potential for some very serious logic bugs, where the program does exactly what the programmer wrote, but what they wrote was not what they intended.

Rust's type safety makes it less vulnerable to these mistakes than some languages, but there is no magic. In C obviously a UID, a PID, a duration, an inode number, a file descriptor, a counter are all just integers. In Rust you could make all those distinct types (the "New type idiom"), and out of the box the Duration and the File Descriptor are in fact provided as distinct types. So, some improvement.

> In C obviously a UID, a PID, a duration, an inode number, a file descriptor, a counter are all just integers. In Rust you could make all those distinct types

For various kinds of IDs you can do that in C, too:

  struct UID {
    int value;
  };
A C compiler can pass these in registers to functions (https://wintermade.it/blog/posts/value-struct.html). So, performance impact should be zero.

It may be not as nice as other languages, but it isn’t bad, either. If you use C++, it can be made a bit nicer, and you could also have such structs that you can calculate with.

It exists, but... throw in some macros or generic cache/storage which is untyped and you end up with a non-trivial version of this:

    struct GID gid = *(GID*) &some_uid;
Which will compile without issues or warnings by default. No belts or braces in this area.
You can technically do this but then you have to write wrapper functions for all relevant syscalls or libc functions to unpack the structure and call the actual thing. Lots of work.
They’re easy to make.