12

Rust is known as a memory-safe language, but there is a security feature in GCC called AddressSanitizer (ASAN):

./configure CFLAGS="-fsanitize=address -g" CXXFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address"
make
make check

Could ASAN provide the same memory safety as Rust, or does Rust have more tricks? Is it even possible to compare the two?

Disclaimer: I am not a programmer.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Hessnov
  • 367
  • 2
  • 7
  • 2
    From that linked article: "On average, the instrumentation increases processing time by about 73% and memory usage by 340%." Which seems like enough reason to not use it by default. – Zan Lynx Feb 20 '18 at 20:28

3 Answers3

21

The sanitizers

Both GCC and Clang have a suite of sanitizers; up until now, they were developed in Clang and then ported to GCC, so Clang has the most advanced versions:

  • Address Sanitizer (ASan): detects out-of-bounds access, use-after-free, use-after-scope, double-free/invalid-free and is adding support for memory leaks (expected memory overhead 3x),
  • Memory Sanitizer (MemSan): detects uninitialized reads (expected slow-down 3x),
  • Thread Sanitizer (TSan): detects data-races (expected slow-down 5x-15x, memory overhead 5x-10x),
  • Undefined Behavior Sanitizer (UBSan): various local undefined behaviors such as unaligned pointers, integral/floating point overflows, etc... (minimal slow-down, slight code size increase).

There is also work ongoing on a Type Sanitizer.


Sanitizers vs Rust

Unfortunately, bringing C++ up to Rust's level of safety with sanitizers is not possible; even combining all existing sanitizers would still leave gaps, they are known to be incomplete.

You can see John Regher's presentation on Undefined Behavior at CppCon 2017, the slides can be found on github, from which we get the current coverage:

enter image description here

And that is not accounting for the fact that sanitizers are incompatible with each others. That is, even if you were willing to accept the combined slow-down (15x-45x?) and memory overhead (15x-30x?), you would still NOT manage for a C++ program to be as safe as a Rust one.


Hardening vs Debugging

The reason sanitizers are so CPU/memory hungry is because they are debugging tools; they attempt to give developers as precise a diagnostic as possible, so as to be most useful for debugging.

For running code in production, what you are looking for is hardening. Hardening is about eliminating Undefined Behavior with as low an overhead as possible. Clang, for example, supports multiple ways to harden a binary:

Those tools can be combined and have minimal (< 1%) performance impact. They cover much less ground than sanitizers, unfortunately, and most notably do not attempt to cover use-after-free/use-after-scope or data-races which are frequent targets of attacks.


Conclusion

I do not see any way to bring C++ up to the level of safety that Rust combines, without either:

  • very serious restrictions on the language: see MISRA/JSF guidelines,
  • very serious loss of performance: sanitizers, disabling optimizations, ...
  • a complete overhaul of the standard library and coding practices, of which the Core Guidelines are a start.

On the other hand, it is worth noting that Rust itself uses unsafe code; and its unsafe code also needs to be vetted (see Rust Belt project) and would benefit from all the above sanitizers/hardening instrumentation passes.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Undefined behavior is not necessary a memory-safety bug, is it? – Zaffy Feb 21 '19 at 15:23
  • @Zaffy: No indeed. In general, memory-safety bugs are generally a consequence of Undefined Behavior. For example, if due to Undefined Behavior a check that a pointer is not NULL is elided, then the program may try to access memory at an offset from NULL which is a memory-safety bug. – Matthieu M. Feb 21 '19 at 15:38
  • I think your example is incorrect. Compiler always assumes UB doesn't occur. So the checks for NULL are optimized away only if the pointer was already dereferenced before, hence if it must not be NULL, otherwise there would've been UB. – Zaffy Feb 21 '19 at 16:11
  • @Zaffy: Actually, it could also be dereference *after* the test, in a portion of the code not covered by the test. In any case, my point is that a memory-safety issue is first and foremost a **symptom** and the cause may not be obviously linked. Another example would be an unaligned pointer causing the compiler to overwrite a couple more bytes than expected, etc... – Matthieu M. Feb 21 '19 at 17:10
  • Actually advanced static analyzers such as PVS Studio and SonarSource detect most of CWE that are not detected by sanitizers that come up with LLVM or GCC. – user6039980 Apr 07 '19 at 23:11
  • @Kais: That's a fairly strong claim, especially given the difficulties inherent to static analysis for inter-procedural issues. I've read quite a few reports of PVS Studio analyzing open source projects, and the issues highlighted seemed mostly centered on small linting things (detecting copy/paste) and a few data-flow analyses (pointer dereferenced before NULL-check); I cannot remember anything inter-procedural. – Matthieu M. Apr 08 '19 at 06:49
  • @MatthieuM. What do you mean by inter-procedural? – user6039980 Apr 08 '19 at 08:09
  • @Kais: Inter-procedural is a term of art meaning "across function boundaries". In general, reasoning within a function (intra-procedural) is much easier than reasoning across function boundaries (inter-procedural); this affects both optimizers and static analyzers. – Matthieu M. Apr 08 '19 at 08:11
  • @MatthieuM. You mean the possible UB caused by the misuse of pointers shared between functions? – user6039980 Apr 08 '19 at 08:15
  • 1
    @Kais: Or in general any lifetime analysis, to prevent use-after-free, double-free, etc... – Matthieu M. Apr 08 '19 at 08:21
  • @MatthieuM. I see, thanks for the clarification. But AFAIK, PVS Studio detects these types of CWE, more specifically in CWE-416 and CWE-415: You can find the rest of supported CWE there: https://www.viva64.com/en/b/0514/. – user6039980 Apr 08 '19 at 09:00
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/191444/discussion-between-matthieu-m-and-kais). – Matthieu M. Apr 08 '19 at 10:48
3

No, the two features are not comparable.

Address sanitization is not a security feature, nor does it provide memory-safety: it's a debugging tool. Programmers already have tools to detect that the code they've written has memory problems, such as use-after-free or memory leaks. Valgrind is probably the best-known example. This gcc feature provides (some of) the same functionality: the only new thing is that it's integrated with the compiler, so it's easier to use.

You wouldn't have this feature turned on in production: it's for debugging only. You compile your tests with this flag, and automatically they detect memory errors that are triggered by the test. If your tests aren't sufficient to trigger the problem, then you still have the problem, and it'll still cause the same security flaws in production.

Rust's ownership model prevents these defects by making programs that contain such defects invalid: the compiler will not compile them. You don't have to worry about your tests not triggering the problem, because if the code compiles, there cannot be a problem.

The two features are for different sets of problems. One feature of address sanitization is to detect memory leaks (allocating memory and neglecting to free it later). Rust makes it harder to write memory leaks than in C or C++, but it's still possible (if you have circular references). Rust's ownership model prevents data races in sequential and multi-threaded situations (see below). Address sanitization doesn't aim to detect either of those cases.

An example of a data race in sequential code is if you're iterating over a collection of objects, while also adding or removing elements. In C++, changing most collections will invalidate any iterators, but it's up to the programmer to realise this has happened: it's not detected (though some collections have extra checks in debug builds). In Rust, it's not possible to mutate the collection while an iterator on it exists, because the ownership model prevents this.

An example of a data race in multithreaded code is having two threads that share an object, with access protected by a mutex. In C++, it's possible for the programmer to forget to lock the mutex while changing the object. In Rust, the mutex itself owns the object it protects, so it's not possible to access it unsafely. (There are many other kinds of concurrency bugs, though, so don't get carried away!)

Dan Hulme
  • 14,779
  • 3
  • 46
  • 95
  • 1
    I don't see how memory leaks are harder to write in Rust than in C++. Both use the same RAII and smartpointer concepts. – CodesInChaos Feb 21 '18 at 11:40
  • @CodesInChaos If you're disciplined in C++ and always use smart pointers, it's hard to write memory leaks - but even today, many shops still rely on getting `new` and `delete` right. The Rust equivalent of smart pointers are the default, and you really have to go out of your way to get `new` and `delete`. – Dan Hulme Feb 21 '18 at 11:43
  • @DanHulme: It's very easy to get memory leaks in Rust. A simple cycle of reference-counted pointers, an ill-timed use of `mem::forget` (which is **safe** following the Leakpocalypse). Rust considers resource leaks safe (memory, file handles, etc...), although it tries its best to help avoid them as they are annoying. – Matthieu M. Feb 22 '18 at 09:56
-1

Haven’t heard of this option, but it sounds like it modifies the output program. In other words, it checks while the program runs.

Rust, on the other hand, checks when the program is created (or compiled in programmer speak), so there isn’t these memory safety bugs in the first place.

The linked article mentions it only covers one case anyway, use after return.

Matthew Curry
  • 732
  • 5
  • 15