1. Early detection. You can't fix a bug that you don't know about.

In C/C++ bugs can easily stay latent, because they may depend on conditions that rarely happen or just be lucky and "asymptomatic" thanks to freed or uninitialized data happening to have acceptable values.

In Rust, when it points out "x used after invalidated", you at least know there's something wrong, so you have a chance to fix it (correctly or not). If you didn't know about the problem, it'd definitely remain incorrect.

2. Ownership, sum types, borrow checking, send/sync thread-safety are not purely memory management features.

So to your argument that memory errors are merely symptoms of other errors, I say that Rust expands features that you classify as mere memory management fixes into broader tools that encompass some types of logic errors too.

For example, if you have a logic bug in code like "do B then A", but logically it should have been "do A, then B", it can have a use-after-free symptom in C/C++ indeed.

In Rust this will have a symptom of use after move compile-time error, where the fix from memory management perspective will have overlap with the fix from logical perspective.

e.g. file.close() can take exclusive ownership. It not only prevents UAF, but also catches logic bugs that manifest in reading file after closing it.

In case it's not clear what I mean, in other words:

closing the same fd twice is not a memory error, and it's harmless from security perspective, but it is a logic error. Idiomatic Rust style "abuses" ownership machinery for enforcing "can't read after close" logical requirement even beyond places that manage memory. So things that have memory-management-like symptoms in Rust can be more directly related to logic constraints expressed using types.

@kornel I had this in mind when writing everything I wrote but I do not conciser closing an fd twice a logic error, it's a resource management error, which for the purposes of this discussion is same as memory error, equivalent to double free. I wrote "resource" in my example intentionally to somehow hint at that. It could be a heap allocated object, an fd, or a container-iterator pair, with the iterator being the part that gets invalidated, or whatever else that fits the pattern. It should really be called resource safety in general, but I was following the convention here.

Btw, in case you didn't know, this resource management generalization comes directly from C++, and has been a thing there since forever.

@namark Sure, Rust copied RAII and moves from C++, but made moves the default, and removed possibility of accessing an empty state left after a moved value.

Rust needs to compare itself to C and C++, because it targets specifically the market cornered by these languages. And yes of course, people who propose Rust as a solution to issues in C/C++ programs do think Rust works better, and the improvement is worth the switch.

Follow

@kornel I didn't mention c++ here to claim it is better in this particular regard, I mention it to call you out on writing paragraph that reads like an ad for resource management generalization in rust, in a thread about comparing rust to c++, which supported that since forever. I didn't mean specifically that rust copied RAII and moves, I mean it copied "whatever you can do with memory you can do to any other resource" generalization.

@namark Thanks. So I see how I could misinterpret your original toot.

My understanding now is:

1. "memory error" category of bugs is not the whole picture => misleading.

2. Rust "patching up with memory management" addresses wrong things.

With 1, I see your point. I would quibble about how often it is the case, but ok.

With 2, I don't agree. It's not "patching up". Rust's features are broader and improve correctness beyond mem management.

@kornel for 2 my point isn't that rust is always wrong, or that all things considered it does a worse job here than c++, my point is that the memory safety alone can't provide a strong guarantee that these errors will be fixes once diagnosed. The error might be fixed, or might be obfuscated, dependent on other factors, such as overall expressiveness of the language or the discipline of the programmer. You can argue that rust is more expressive or coherent, but that would be a completely different, much more subjective and complicated discussion, compared to the basic sales pitch of "all bajillion of these memory* errors I counted will 100% entirely go away".

@namark Rust doesn't promise 100% fix of course, but it does promise (and IMHO deliver) a substantial reduction that is worth talking about.

Predicting whether a bug detected through safety features will be truly properly diagnosed and fixed entirely, is too speculative to argue with. You're pessimistic. I'm optimistic. I don't think I can convince you here, because it's about predicting future behavior of hypothetical people fixing hypothetical issues. My imaginary rustaceans do it well 😀

@namark I can tell you that I did feel change in types of bugs I wrote when I switched from C to Rust. In my experience these features do help clarify thinking about data flow in the program, its architecture, and provide good blocks and vocabulary for using threads reliably. I used to debug runtime bugs & fudge things until they stopped crashing. I now I think in terms of data flow instead. I haven't gotten smarter, but my bugs changed from memory sprays to missing features and asserts firing.

@kornel it's not that hard for a language to be more expressive than C. I'm glad you upgraded.

@namark Besides a perspective that a memory bug patched may not mean the real bug there is truly fixed, there's another POV where this doesn't matter.

Security-focused projects value memory protection, canaries, sandboxing, process isolation, and other hardening/mitigations. These features do *literally nothing* about bugs, and yet are treated as immensely important. Rust's pitch of addressing memory safety (even if superficially) makes sense in that context. It's another layer of hardening.

@kornel @namark Yayay

how do I make a rude ass stranger go kek for an audio sample?
Kekrkekkek works too. I need 5 seconds, no killing.

I expect you to expect me to deserve a Xbox One X from Santa with No More Heroes 2 and 3. PLz TV is cancer ♋

@namark e.g. from perspective of Microsoft (source of memory error bug statistic).

If they fail to do a bounds check on a button label length, that's going to be 0day vuln, which may literally put lives at risk, and embarrass the company again.

Automatically inserted bounds check on this label stops the 0day, and may not even need proper fix in practice if all normal uses use humanely-short labels. It is a latent technically-unfixed bug, but a significant improvement regardless.

@kornel rather unfortunate that you chose a buffer overrun as an example, as rust provides no static guarantees and only runtime guarantees for that. An equivalent can be achieved with standard library implementation in c++. Here is a proof of concept: gcc.gnu.org/onlinedocs/libstdc
It's not exactly meant for production so it does way more than just bound checking, but a similar thing could be tuned to whatever needs. It's also ABI compatible, so you don't have to re-implement the whole worlds or revert back to C level for inter-op with the existing unsafe implementations. It makes no sense to introduce this runtime overhead across the board as opposed to opt-in, because it goes againt one of the core principles of the language, which rust often also is marketed to adhere to, until someone points out that it doesn't in this specific case and make the salesmen cry.

If you have any other examples I'm interested, since otherwise I have no respect for any sort of common industry practices, and whatever backwards reasoning they follow, especially with regards to security.

@namark Haha, it is a runtime issue indeed.

This case is more subtle than "Rust can, C++ can't".

C++ has risky defaults, and programmers overconfidently use them.

vector[] is unchecked in release builds. Be serious—nobody ships debug mode binaries. People *could* use .at(), but they don't, because they "know what they're doing".

Rust also has checked/unchecked variants, so this isn't a technical difference, but a matter of guiding people towards safer apis. C++ could do it, but doesn't.

@namark Another example is dereference of std::optional. C++ chose to make it UB. It's a new feature, not a back-compat requirement, and yet C++ still added a new way to have UB in the name of performance.

Rust's equivalent Option will not let you do such risky thing with an innocently-looking *.

I suspect you will not agree with me on the difference, because it's not a technical one.

But all C++ programs are 100% safe if nobody writes bugs, so the issue is really about people, not just tech.

@kornel I'm not saying you should ship the debug libraries I linked, I just wanted to show that as proof of concept alternative implementation of standard containers. You can have much simpler implementations that only do bounds checking, and those probably exists, I just never needed anything like that outside of debug builds, so I don't know of any other examples. The point being that this risky default is an implementation detail and not core issue of the language. You do not need a different language to fix this particular risky default, if fixing it is proven to be important for your use case.

So what I'm asking for is an example of logic error that causes a memory error that rust can identify statically, for which in context of a "security-focused project" a fix of the memory error that does not address the logic error will be strictly better than not fixing anything at all. Quite the mouthful, but I'm just repeating the context here to be clear.

Can't think of anything like that with optional, but I'll rant about it anyway, cause it's interesting, and an easy example to explain what you call "risky defaults". You write "in the name of performance" as if that's not one of the core principles of the language that it promises to its entire user base. The standard library APIs can't be inefficient. The implementation can be, you can beat either libstdc++ or libc++ or whatever microsoft has (especially whatever microsoft has) for you own purposes, but reusing the same API. There should be nothing in the API that dictates an inefficient implementation or use, because if there is such a thing, if there is a more efficient API, the c++ user base will implement it and use it. The end result is everyone has their own incompatible versions of one or the other type or function.

std::optional is designed in context of c++ to replace a common c++ pattern
struct {
T value;
bool exists;
};
(often abbreviated as std::pair<bool, T>, cause who wants to write a struct?)
with an invariant that the value is initialized if the exists is true. It's not meat to replicate whatever hip programming style, though that might happen in time. The primary purpose is to ensure that nobody has any good excuse to write any version of that struct ever again anywhere - safe code, unsafe code, high performance code, small size code, CPU, GPU, microcontroller, super computer, FPGA or what have you - no excuses, use optional. You need something safer - wrap optional, never write that freakin struct, unless you are implementing an API and preferably ABI compatible replacement for std::optional. The major goal of the standard library is to be a common ground for all codebases, to make code more expressive and readable and minimize dialects. Not that c++ doesn't have dialects galore, but that's not by design, in contrast to rust which requires a dialect by design.

I guess what you expected instead is a static guarantee that the not-exists case will be handled. That's conceptually closer to a tagged union
struct {
union {
T value;
std::monostate no_value;
};
enum tag index;
}
The equivalent of which would be std::variant<std::monostate, T>, which can provide the static guarantee as long as you stick to std::visit, but again its main goal still is to be a replacement of all uses of the tagged union in all contexts, not directly matching patterns (hehe) from other languages. Now variant incidentally fails at this
stackoverflow.com/questions/48
encouraging people to use workarounds or implement their own tagged unions. This must be a great boon to you, as c++ failure is essential for you to justify rust. The OP is pretty much saying "stop dunking on c++" (albeit in provocative fashion) and you and another fan here jump in with "nooo, I must continue dunking on c++, cause otherwise my bubble is gonna burst". But if you are going to do that at least understand the goals of the language you are dunking on, which is to be expressive and coherent across the board, to make the logical invariants as clear as possible and consistent in all code, and make sure that as long as you diligently follow them, the program will be correct and performant in every way, including (but not limited to) resource management. And if you can not follow the logical invariants (not because your indisputable stupidity and lazyness, but because they are not expressed clearly enough), no amount of guaranteed resource safety (in a subset of your language) is going to save your from the logic errors, that you are now trying to argue are objectively better than memory errors for "security", because you just can't let it go.

@namark There are lots of good things that C++ could do in theory, but in practice it won't. These are so heavily entrenched that it's easier to change to Rust than to change what C++ users tolerate (e.g. see why Google wanted to fork C++). Bounds checks on by default in shipped release builds are one of these. It'd cause a revolt.

@namark Thing that Rust finds statically: use-after-free.

The `if (cond) consume(it); use(it)` is caught statically in Rust. You say it could be fixed incorrectly. UAF is exploitable, but badly fixed logic may or may not be.

Related reading:

docs.google.com/document/d/e/2

@namark And as for optional. In Rust tagged unions and optionals are unified.

enum Option<T> {
None, Some(T)
}

but Rust has a concept of a "niche", and if it sees that T has some invalid bit patterns, then chooses them to be used as the None. In case of non-null reference, it means Option<&T> is identical (and ABI-compatible) to null pointers.

Rust's option has no difference in perf compared to C++. There is difference in how syntactically hard it is to cause UB.

@namark All bugs are written by "bad" programmers, so safety is an issue of guiding devs away from easy-to-trigger footguns.

Rust does it by giving safe constructs nice syntax, unsafe constructs ugly syntax.

`*opt` is `unsafe { *opt.get_unchecked() }` in Rust. It is important that it stands out in code reviews.

In practice it's not used, because `if let Some(nonopt) = opt` has a cost of `if opt != nullptr`, and the unwrapped variable there is then usable without further checks.

@kornel Yes that's how static guarantees work, everyone knows that. C++ provides the same exact static guarantees for variant, using std::visit, and that seems to be your complaint - that visit doesn't work with optional, but you don't know enough c++ to word it correctly. That is hardly a fundamental philosophical conflict, it's simply an omission, nothing stops visit from being overloaded for optional or even raw pointers if you want the same guarantees with those, and regarding the efficiency of unsafe operator * you are again missing a point. Static grantee has a cost of a null check in some situations where compiler can't proove them to be unnecessary. This is not freaking javascript - I'm not afraid of a null check (or omg a function call overhead) in a getter function to paranoically create a local every time I need to refer to a field/property. it's about situations where were a single top level null check is completely unnecessary and the compiler can't prove it. That's when you use operator * without a explicit check, that safe interfaces like value(), value_or() or equivalent of std::visit dictate. Especially when you write a generic code that you expect everyone to use to make the language more coherent. Again nobody should have an excuse to not use it, and even small inefficiency is an excuse for someone, cause it would be in their hot path.

Let's now deconstruct the justification you present for the butt ugly unsafe optional access in rust that you so kindly demonstrated.
>It is important that it stands out in code reviews.
Why? I see two possible reasons here:
1. To slap that pesky intern on the wrist for trying to use "unsafe" code without first earning the right, which is reserved for the elite class of programmers only (as in c++ programmers)
2. You respect your coworkers decision, but want to pay extra attention to this clearly necessary, but complicated and hard to reason about code.

If it's 1, then congratulations you will get a lot of support form corporate executives who who treat their workforce as commodity.
If it's 2, then why in the world do you make the supposedly most crucial part of your program to get right, unnecessarily verbose, hard to read and write. It's like "ah yes I magically have more time to review this, now that unsafe keyword is used". No you don't, the harder it is to reason about the more expressive and simple it needs to be. Everyone who spent any amount of time with C or C++ knows what * and -> are and how they are not safe. They even might now of some common patterns and anti-patterns. These should not be used often in modern c++ code and they would definitely be noticed in a proper code review, even is relatively old code based. "Oh no but I might not actually notice the tiny little star, and I need to be able to grep for unsafe, cause I'm not actually reviewing anything, I'm just looking for excuses to bash the intern". A single character difference that changes a reference to a pointer is notices in c++ code review and discussed in length, if it's a real code review, because you are looking for logical patterns using your brain, it's not an action game with a bunch stars wheezing around the screen for you to catch.

Finally I thought you are full on tangent mode with the optional stuff, but now you want to imply it's UAF? UAF that rust handles that c++ can't is a completely different thing that is about resource management. And did you even read the google doc you are linking to? It almost repeats after me and you think it's an argument in your favor in this discussion? I guess it's not a mere bubble it's an explosively hydroformed steel sphere:
yewtu.be/watch?v=Sk9WyEfzWPg

Literally the points in the article.
1. buffer overrun runtime checks is a non issue, as in you don't need to change the language spec to deploy them wherever you want however you want. It's a runtime thing either way.
2. UAF, UAM type problems can be solved through use of better abstractions with clear invariants that are easy to adhere to.
"We believe that, given sufficient tolerance for micro-efficiency regressions, we could essentially eliminate spatial unsafety in C++ in first-party code. We could do this (and have started doing so) with a combination of library changes and additions, compiler options, and policies/style rules and presubmit checks (including banned and encouraged classes and constructs)." As in - you do not need a borrow checker that prevents you from properly using a linked list in order to write resource safe code.

"Micro-efficient and ergonomic temporal safety remains an open problem" as in rust is not efficient and ergonomic, by the standards of c++.

Nothing in the article suggest that UAF is always exploitable, or that it is worse than a logic error in your program. You are the only one trying to argue that, repeatedly asserting without explanation and presenting completely unrelated examples and arguments about general usability and expressiveness of the languages, which lead me to conclude you have no actual argument with regards to the OP left.

@namark My std::optional complaint is not about lack of unification with variant (that's merely a clunky language design) but about adding more UB to the language *under an easy to use innocent-looking syntax*
Exceptional case of bypassing the check should have exceptional syntax, so that it can't be done by accident, and that the danger stands out in code reviews. Rust and C++ approaches have same perf and same features, but the difference is how they surface risky vs benign features to users.

@kornel Thank for not reading my point on code review, must be a lot of fun playing code reviews by 360 no-scoping unsafe code.

@namark My masto instance has a low char limit, so I see only half of your messages + I have to respond in small chunks. Hold your horses.

@kornel take your time, read it all, reply whenever, but after reading all plz

@namark Every safety bug is written by a human, who thought they haven't written a safety bug. So "just don't write bugs" by definition doesn't work. Every language is perfectly safe if you always use it correctly.

"Slapping an intern" is a good feature. If you don't trust them, you can tell them not to use unsafe, and/or review their unsafe with an extra level of paranoia.

@namark
unsafe {} blocks help code reviews, whether of interns or elite.

In Rust, outside of `unsafe{}` blocks, every * and . is safe. This is a time-saver, because there you can focus on following logic/perf/features/etc, and not "can this be null? dangling? out of bounds? has this been moved out of?"

Rust also has a concept of encapsulating unsafety: safe & checked abstraction on top of unsafe code. So you can impl a risky pattern once, then have reusable foolproof interface for interns

@kornel ok so, confirm no respect of coworkers, including no trust for interns to not write unsafe when you told them to not write unsafe. And confirm on making the most important part that you want to review most carefully hardest to read. Excellent argument.

@namark Framing this as disrespect is disingenuous. You can respect your coworkers, and help verify their work in a supportive, collaborative manner. It's a teamwork with a shared goal, not a pissing contest.

@namark Also it's absolutely normal to have code reviews for any project in any language. It's true for C++ too, and especially true for security-focused projects.

@kornel I tell you to not use operator * on optional and If I trust you, that is all. If I don't trust you I design a tool that will mechanically 100% guarantee you will not use it, because I can't be bothered to review your code. You appear to think it's difficult to not use an operator when you are told to. Well I guess it is if you are writing code by copy pasting.

@namark This framing doesn't sound to me as emphatic, but rather combative, and framing bugs as a flaw of character, and implying incompetence.

Well-meaning, knowledgeable, and skilled people can make mistakes too.

We can't expect code to be always written by elite programmers at top of their game, without ever making a single mistake.

Sometimes we forget. Sometimes we're in a rush. Sometimes we work with less experienced people, and would like them to be productive without undue risk.

@namark

And when people write bugs, it's not always an obvious local mistake from lack of understanding.

For example, a correct program may have had always-available data behind a pointer, so * was correct and safe. Requirements changed, data became optional, so it was changed to std::optional. Now, * changes its meaning and safety implications. Of course author of the change must review and fix all uses, but could miss some.

@namark And when someone makes a mistake, it's better for everyone involved to have the compiler catch it, rather than have a bug, and a round of "you suck" gloating from asshole "elite" programmers who think they certainly would never write such a stupid bug.

Show newer

@namark You lost me about UAF and hydroformed steel.

As for Google doc, notice that approach they went with is MiraclePtr which adds runtime overhead to mitigate errors that Rust catches at compile time.

They've also explored implementing a borrow checking abstraction in C++, but found they can't do it without compiler help and changes to C++ dtor semantics.

security.googleblog.com/2021/0

@namark Nothing generalizes to absolutes, so I'm nto saying "all UAF everywhere always all the time no exceptions period" is exploitable. However, UAF very often is a key piece in exploits, because when allocator reuses recently freed memory, the new use may give attacker ability to trigger allocation of something new there, which they have access to, and exploit type confusion to write to this memory, which will still be interpreted as old type in UAF.

@namark That's why classic mitigations for C/C++/Zig include allocators that segregate memory by struct type, or delay deallocation.

@kornel I meant that the article is arguing directly against you but you are so entrenched in your misconceptions that you can't see it. Read it again, read the quotes that I pointed out and my comments.

Yes you can't have a borrow checker in c++ across the board, because it goes against the principles of the language, and it's standards of efficiency and ergonomics. Rust did not succeed at the same thing, all it did is abandon the standards of efficiency and ergonomics, just like any other language that came before. I know it has been advertised to you as only leaving the pesky backwards compatibility behind, but that's not the whole story, and anyone who seriously studies both languages knows this.

@namark In terms of ergonomics Rust is indeed controversial, and at very least difficult to learn if you're used to lax languages.
However, I'd say that ergonomics of writing correct programs, especially with multi-threading involved, are much better in Rust thanks to compiler help.

Efficiency is fine. Rust has stricter aliasing than C++. Rust's moves are more efficient than moves in C++ that run dtor on leftover instance. Rust's smart pointers don't have ABI issues. Option can avoid a tag.

@kornel I spelling out the conclusion from the article you linked, and you are responding to it with "it's fine". Alright it's fine. Java is also faster than c++ cause it can analyze and optimize memory allocation strategy of you spaghetti code at runtime. Sure. Count your blessings. I'm talking about the broader scope and the philosophy of the language.

And yes UAF is exploitable, I didn't say it isn't, but it depends on the program logic, just like the exploit-ability of a logic error. Not to mention that the idea of substituting something for the freed memory is very much specific to modern multi-user system which are notoriously insecure by design. Optimizing mitigation for fundamental flaws of some underlying systems is not exactly a good long term goal for a language, in my opinion.

Show newer
Sign in to participate in the conversation
Qoto Mastodon

QOTO: Question Others to Teach Ourselves
An inclusive, Academic Freedom, instance
All cultures welcome.
Hate speech and harassment strictly forbidden.