Hacker News new | past | comments | ask | show | jobs | submit | page 2 login
This shouldn't have happened: A vulnerability postmortem (googleprojectzero.blogspot.com)
938 points by trulyrandom 50 days ago | hide | past | favorite | 481 comments


We continue to be reminded that it's hard to write fully memory secure code in a language that is not memory secure?

And by hard, I mean, very hard even for folks with lots of money and time and care (which is rare).

My impression is that Apple's imessage and other stacks also have memory unsafe languages in the api/attack surface, and this has led to remote one click / no click type exploits.

Is there a point at which someone says, hey, if it's very security sensitive write it in a language with a GC (golang?) or something crazy like rust? Or are C/C++ benefits just too high to ever give up?

And similarly, that simplicity is a benefit (ie, BoringSSL etc has some value).

It's hard to fault a project written in 2003 for not using Go, Rust, Haskell, etc... It is also hard to convince people to do a ground up rewrite of code that is seemingly working fine.

> It is also hard to convince people to do a ground up rewrite of code that is seemingly working fine.

I think this is an understatement, considering that it's a core cryptographic library. It appears to have gone through at least five audits (though none since 2010), and includes integration with hardware cryptographic accelerators.

Suggesting a tabula rasa rewrite of NSS would more likely be met with genuine concern for your mental well-being, than by incredulity or skepticism.

> Suggesting a tabula rasa rewrite of NSS would more likely be met with genuine concern for your mental well-being, than by incredulity or skepticism.

In my experience, porting code more or less directly from one language to another is faster and easier than people assume. Its certainly way faster than I assumed. I hand ported chipmunk2d to javascript a few years ago. Its ~30k LOC and it took me about a month to get my JS port working correctly & cleaned up. I spend up throughout the process. By the end I had a bunch of little regexes and things which took care of most of the grunt work.

If we assume that rate (about 1kloc / person-day) then porting boringssl (at 356kloc[1]) to rust would take about one man-year (though maybe much less). This is probably well worth doing. If we removed one heartbleed-style bug from the source code on net as a result, it would be a massive boon.

(But it would probably be much more expensive than that because the new code would need to be re-audited.)

[1] https://www.openhub.net/p/boringssl/analyses/latest/language...

> In my experience, porting code more or less directly from one language to another is faster and easier than people assume.

That's often true right up to the point where you have to be keenly aware of and exceptionally careful with details such as underlying memory management functionality or how comparisons are performed. With this in mind, cryptographic code is likely a pathological case for porting. It would be very easy to accidentally introduce an exploitable bug by missing, for example, that something intentionally reads from uninitialized memory.

On top of the re-audit being expensive.

> for example, that something intentionally reads from uninitialized memory.

Sounds terrible. This should never happen in any program, so any behavior relying on it is already broken.

I'm way more concerned by memory safety issues than cryptographic issues. Frankly, history has shown that cryptographic bugs are far easier to shake out and manage than memory safety bugs.

> Frankly, history has shown that cryptographic bugs are far easier to shake out and manage than memory safety bugs.

and yet, we had the debian/ubuntu openssl bug of 2008... due to someone not wanting to intentionally read from uninitialized memory. Really, it kind of proved the opposite. Valgrind and other tools can tell you about memory safety bugs. Understanding that the fix would result in a crypto bug was harder.

OpenSSL's use of uninitialized memory to seed entropy was always a terrible idea. The PRNG was fundamentally flawed to begin with.

> Really, it kind of proved the opposite.

Not really. Exploited bugs in cryptographic protocols are extremely rare. Exploited memory safety bugs are extremely common.

> Valgrind and other tools can tell you about memory safety bugs.

Not really.

> Understanding that the fix would result in a crypto bug was harder.

Like I said, OpenSSL's PRNG was brutally flawed already and could have been broken on a ton of machines already without anyone knowing it. A compiler update, an OS update, or just unluckiness could have just as easily broken the PRNG.

Building memory unsafety into the prng was the issue.

Memory safety issues are exploited orders of magnitude more often than crypto bugs.

edit: Also, memory safety bugs typically have higher impact than crypto bugs. An attacker who can read arbitrary memory of a service doesn't need a crypto bug, they can just extract the private key, or take over the system.

Crypto bugs are bad. Memory safety bugs are way, way worse.

If a programs reads from uninitialised memory, I hope for its sake that it does not do it in C/C++. Setting aside that uninitialised memory is a hopelessly broken RNG seed, or the fact that the OS might zero out the pages it gives you before you can read your "uninitialised" zeroes…

Reading uninitialised memory in C/C++ is Undefined Behaviour, plain and simple. That means Nasal Demons, up to and including arbitrary code execution vulnerabilities if you're unlucky enough.

Genuinely curious what the use case(s) of reading from uninitialized are. Performance?

It was used as a source of randomness. Someone blindly fixing a "bug" as reported by a linter famously resulted in a major vulnerability in Debian: https://www.debian.org/security/2008/dsa-1571

This is incorrect.

If they had simply removed the offending line (or, indeed, set a preprocessor flag that was provided explicitly for that purpose) it would have been fine. The problem was that they also removed a similar looking line that was the path providing actual randomness.

> In my experience, porting code more or less directly from one language to another is faster and easier than people assume

Converting code to Rust while keeping the logic one-to-one wouldn't work. Rust isn't ensuring memory safety by just adding some runtime checks where C/C++ aren't. It (the borrow checker) relies on static analysis that effectively tells you that the way you wrote the code is unsound and needs to be redesigned.

Sounds like a feature of the porting process. Not a bug. And I’d like to think that BoringSSL would be designed well enough internally to make that less of an issue.

I agree that this might slow down the process of porting the code though. I wonder how much by?

The article says Chromium replaced this in 2015 in their codebase. (With another memory-unsafe component, granted...)

BoringSSL started as a stripped down OpenSSL. That's very different from a ground-up replacement. The closest attempt here is https://github.com/briansmith/ring but even that borrows heavily the cryptographic operations from BoringSSL. Those algorithms themselves are generally considered to be more thoroughly vetted than the pieces like ASN.1 validation.

This sounds like a nightmare for any downstream users of this library. Any one of those bullet points in that section would be a major concern for me using it in anything other than a hobby project, but all of them together seem almost willfully antagonistic to users.

This is especially true given it’s a security library, which perhaps more than any other category I would want to be stable, compatible, and free of surprises.

“You must upgrade to the latest release the moment we release it or else you risk security vulnerabilities and won’t be able to link against any library that uses a different version of ring. Also, we don’t ‘do’ stable APIs and remove APIs the instant we create a new one, so any given release may break your codebase. Good luck have fun!”

Note that the readme is outdated. With the upcoming 0.17 release, which is in the making for almost a year already, you can link multiple versions of ring in one executable: https://github.com/briansmith/ring/issues/1268

Similarly, while the policy is still that ring only supports the newest rust compiler version, due to the fact that there has been no update for months already, you can use it with older compiler versions.

Last, the author used to yank old versions of its library, which caused tons of downstream pains (basically, if you are a library and are using ring, I recommend you have a Cargo.lock checked into git). This yanking has stopped since 3 years already, too. Don't think this was mentioned in the readme, but I feel it's an important improvements for users.

So a great deal of things has improved, although I'd say only the first bullet point is a permanent improvement, while the second two might be regressed upon. idk.

Yeah, that is pretty wild. Total prioritization of developer convenience over actual users of the library.

Or rust-crypto

nss was also generally considered to be thoroughly vetted though

There’s a world of difference between ASN.1 validation and validation of cryptographic primitives. The serialization/deserialization routines for cryptographic data formats or protocols are where you typically get problems. Things like AES and ECDSA itself, less so, especially when you’re talking about the code in BoringSSL. Maybe some more obscure algorithms but I imagine BoringSSL has already stripped them and ring would be unlikely to copy those.

Why? Cryptographic primitives don’t really have a lot of complexity. It a bytes in/bytes out system with little chance for overflows. The bigger issues are things like side channel leaks or incorrect implementations. The former is where validation helps and the latter is validated by round-tripping with one half using a known-working reference implementation. Additionally, the failure mode is typically safe - if you encrypt incorrectly then no one else can read your data (typically). If you decrypt incorrectly, then decryption will just fail. Ciphers that could encrypt in an unsafe way (ie implementation “encrypts” but the encryption can be broken/key recovered) typically implies the cipher design itself is bad and I don’t think such ciphers are around these days. Now of course something like AES-GCM can still be misused by reusing the nonce but that’s nothing to do with the cipher code itself. You can convince yourself by looking for CVEs of cryptographic libraries and where they live. I’m not saying it’s impossible, but cipher and digest implementations from BoringSSL seem like a much less likely place for vulnerabilities to exist (and thus the security/performance tradeoff probably tilts in a different direction unless you can write code that’s both safer while maintaining competitive performance).

For symmetric cryptography (ciphers & hashes), I agree. I'd say as far as to say they're stupidly easy to test.

Polynomial hashes, elliptic curves, and anything involving huge numbers however are more delicate. Depending on how you implement them, you could have subtle limb overflow issues, that occur so extremely rarely by chance that random test don't catch them. For those you're stuck with either proving that your code does not overflow, or reverting to simpler, slower, safer implementation techniques.

That's a very good point. Thanks for the correction!

The Ring readme doesn't really cover its functionality but sounds like it may be a lower level crypto lib than NSS? And it also seems to be partly written in C.

Anyway, NSS wouldn't necessarily need to be replaced with a Rust component, it could well be an existing lib written in another (possibly GCd) safeish language, or some metaprogramming system or translator that generated safe C or Rust, etc. There might be something to use in Go or .net lands for example.

ring incorporates a barebones ASN.1 parser that also webpki uses, which is probably the crate you want to use if you want to do certificate verification in Rust. webpki is C-free but it does use ring for cryptographic primitives so that will have to be replaced if you don't like ring. More generally, I think Firefox wants to have great control over this specific component so they likely want to write it themselves, or at least maintain a fork.

Perl generated assembly. Hehh…

> Suggesting a tabula rasa rewrite of NSS would more likely be met with genuine concern for your mental well-being, than by incredulity or skepticism.

Why? I don't get it. Maintenance of NSS has to be seriously expensive.

The NSA was caught intentionally complicating that spec. The idea was to ensure it was impossible to implement correctly, and therefore be a bottomless well of zero days for them to exploit.

Gotta love the US government’s war against crypto.

Sorry for sounding like a broken record, but source please?

Can you provide more information on this? I'd be interested to read about this topic.

He's confused it with Dual_EC_DRBG, a backdoored random number generator in a different non-international standard.

SSL is complicated because we didn't understand how to design secure protocols in the 90s. Didn't need help.

No; this predated that by about a decade. They had moles on the committees that codified SSL in the 90’s. Those moles added a bunch of extensions specifically to increase the likelihood of implementation bugs in the handshake.

I’m reasonably sure it was covered in cryptogram a few decades ago. These days, it’s not really discoverable via internet search, since the EC thing drowned it out.

Edit: Here’s the top secret budget for the program from 2013. It alludes to ensuring 4G implementations are exploitable, and to some other project that was adding exploits to something, but ramping down. This is more than a decade after the SSL standards sabotage campaign that was eventually uncovered:


With SSL, the moles kept vetoing attempts to simplify the spec, and also kept adding complications, citing secret knowledge. It sounds like they did the same thing to 4G.

Note the headcount numbers: Over 100 moles, spanning multiple industries.

To be fair you don't need to rewrite the whole thing at once. And clearly the audits are not perfect, so I don't think it's insane to want to write it in a safer language.

It may be too much work to be worth the time, but that's an entirely different matter.

> may be too much work

I wonder, how many work years could be too much

(What would you or sbd else guess)

>seemingly worked fine

That’s just it though, it never was. That C/C++ code base is like a giant all-brick building on a fault line. It’s going to collapse eventually, and your users/the people inside will pay the price.

>>seemingly worked fine

>That’s just it though, it never was. That C/C++ code base is like a giant all-brick building on a fault line. It’s going to collapse eventually, and your users/the people inside will pay the price.

Sure, but everything is a trade-off[1]. In this particular case (and many others) no user appeared to pay any price, which tells me that the price is a spectrum ranging from 'Nothing' to 'FullyPwned' with graduations in between.

Presumably the project will decide on what trade-off they are willing to make.

[1] If I understand your comment correctly, you are saying that any C/C++ project has a 100% chance of a 'FullyPwned' outcome.

What's somewhat interesting is memory safety is not a totally new concept.

I wonder if memory safety had mattered more, whether other languages might have caught on a bit more, developed more etc. Rust is the new kid, but memory safety in a language is not a totally new concept.

The iphone has gone down the memory unsafe path including for high sensitivity services like messaging (2007+). They have enough $ to re-write some of that if they had cared to, but they haven't.

Weren't older language like Ada or Erlang memory safe way back?

Memory safe language that can compete with C/C++ in performance and resource usage is a new concept.

AFAIK ADA guarantees memory safety only if you statically allocate memory, and other languages have GC overhead.

Rust is really something new.

There's different classes of memory un-safety: buffer overflow, use after free, and double free being the main ones. We haven't seen a mainstream language capable of preventing use and free and double free without GC overhead until Rust. And that's because figuring out when an object is genuinely not in use anymore, at compile time, is a really hard problem. But a buffer overflow like from the article? That's just a matter of saving the length of the array alongside the pointer and doing a bounds check, which a compiler could easily insert if your language had a native array type. Pascal and its descendants have been doing that for decades.

> That's just a matter of saving the length of the array alongside the pointer and doing a bounds check, which a compiler could easily insert if your language had a native array type. Pascal and its descendants have been doing that for decades.

GCC has also had an optional bounds checking branch since 1995. [0]

GCC and Clang's sanitisation switches also support bounds checking, for the main branches, today, unless the sanitiser can't trace the origin or you're doing double-pointer arithmetic or further away from the source.

AddressSanitizer is also used by both Chrome & Firefox, and failed to catch this very simple buffer overflow from the article. It would have caught the bug, if the objects created were actually used and not just discarded by the testsuite.

[0] https://gcc.gnu.org/extensions.html

> It would have caught the bug, if the objects created were actually used and not just discarded by the testsuite.

They were only testing with AddressSanitizer, not running the built binaries with it? Doing so is slow to say the least, but you can run programs normally with these runtime assertions.

It even has the added benefit of serving as a nice emulator for a much slower system.

> We haven't seen a mainstream language capable of preventing use and free and double free without GC overhead until Rust.

Sorry, that just isn’t the case. It is simple to design an allocator that can detect any double-free (by maintaining allocation metadata and checking it on free), and prevent any use-after-free (by just zeroing out the freed memory). (Doing so efficiently is another matter.) It’s not a language or GC issue at all.

> prevent any use-after-free (by just zeroing out the freed memory)

It's not quite that simple if you want to reuse that memory address.

Not reusing memory addresses is a definite option, but it won't work well on 32-bit (you can run out of address space). On 64-bit you may eventually hit limits as well (if you have many pages kept alive by small amounts of usage inside them).

It is however possible to make use-after-free type-safe at least, see e.g. Type-After-Type,


Type safety removes most of the risk of use-after-free (it becomes equivalent to the indexes-in-an-array pattern: you can use the wrong index and look at "freed" data but you can't view a raw pointer or corrupt one.). That's in return for something like 10% overhead, so it is a tradeoff, of course.

Rust is a definite improvement on the state of the art in this area.

One of the things I like about Zig is that it takes the memory allocator as a kind of “you will supply the correct model for this for your needs/architecture” as a first principle, and then gives you tooling to provide guarantees downstream. You’re not stuck with any assumptions about malloc like you might be with C libs.

On the one hand, you might need to care more about what allocators you use for a given use case. On the other hand, you can make the allocator “conform” to a set of constrictions, and as long as it conforms at `comptime`, you can make guarantees downstream to any libraries, with the a sort of fundamental “dependency injection” effect that flows through your code at compile time.

Zig is, however, not memory safe, which outweighs all of those benefits in this context.

It can be memory safe. It's up to you to choose memory safety or not. That's a piece of what I was getting at. Unless I misunderstand something. I've only dabbled with Zig.

I'm not aware of any memory safety that works in Zig, other than maybe Boehm GC. The GeneralPurposeAllocator quarantines memory forever, which is too wasteful to work in practice as one allocation is sufficient to keep an entire 4kB page alive.

I mean, if you don't care about efficiency, then you don't need any fancy mitigations: just use Boehm GC and call it a day. Performance is the reason why nobody does that.

Zeroing out freed memory in no way prevents UAFs. Consider what happens if the memory which was freed was recycled for a new allocation? Maybe an example will help make it clearer? This is in pseudo-C++.

    struct AttackerChosenColor {
        size_t foreground_color;
        size_t background_color;
    struct Array {
        size_t length;
        size_t *items;

    int main() {
    // A program creates an array, uses it, frees it, but accidentally forgets that it's been freed and keeps using it anyway. Mistakes happen. This sort of thing happens all of the time in large programs.
    struct Array *array = new Array();
    free(array); // Imagine the allocation is zeroed here like you said. The array length is 0 and the pointer to the first item is 0.
    struct AttackerChosenColor *attacker = new AttackerChosenColor();
    // The allocator can reuse the memory previously used for array and return it to the attacker. Getting this to happen reliably is sometimes tricky, but it can be done.

    // The attacker chooses the foreground color. They choose a color value which is also the value of SIZE_T_MAX.
    // The foreground_color *overlaps\* with the array's length, so when we change the foreground color we also change the array's size.
attacker->foreground_color = SIZE_T_MAX; // The background_color overlaps with the array's size, so when we change the background color we also change the array's start. // The attacker chooses the background color. They choose a color value which is 0. attacker->background_color = 0;

    // Now say the attacker is able to reuse the dangling/stale pointer.
    // Say that they can write a value which they want to wherever they want in the array. This is 
    // Like you suggested it was zeroed when it was freed, but now it's been recycled as a color pair and filled in with values of the attacker's choosing.
    // Now the attacker can write whatever value they want wherever they want in memory. They can change return addresses, stack values, secret cookies, whatever they need to change to take control of the program. They win.
    if (attacker_chosen_index < array->length) {
         array->items[attacker_chosen_index] = attacker_chosen_value;

> Zeroing out freed memory in no way prevents UAFs.

Maybe they meant it zeroes out all the references on free? This is possible if you have a precise GC, although not sure if it's useful.

The trick is not that the language support a safe approach (C++ has smart pointers / "safe" code in various libraries) in my view but simply that you CAN'T cause a problem even being an idiot.

This is where the GC languages did OK.

As far as I know, nothing in the C/C++ standard precludes fat pointers with bounds checking. Trying to access outside the bounds of an array is merely undefined behavior, so it would conform to the spec to simply throw an error in such situations.

There's address-sanitizer, although for ABI compatibility the bound is stored in shadow memory, not alongside the pointer. It is very expensive though. A C implementation with fat pointer would probably have significantly lower overhead, but breaking compatibility is a non-starter. And you still need to deal with use-after-free.

I believe that's how D's betterC compiler [0] works, whilst retaining the memory safe features.

[0] https://dlang.org/spec/betterc.html

> But a buffer overflow like from the article? That's just a matter of saving the length of the array alongside the pointer and doing a bounds check, which a compiler could easily insert

Both the array length and the index can be computed at runtime based on arbitrary computation/input, in which case doing bounds checks at compile time is impossible.

What new concept? When C and C++ appeared they were hardly usefull for game development, hence why most games were coded in Assembly.

After C, alongside TP, got a spot on languages accepted for game development, it took about 5 years for C++ to join that spot, mostly triggered by Watcom C++ on the PC, and PS2 SDK.

There was no GC overhead on the systems programming being done in JOVIAL, NEWP, PL/I,...

There were OSe written in memory safe languages before two persons decided to create a toy OS in Assembly for their toy PDP-7.

The issue isn't really that there was a shortage of memory safe languages, it's that there was a shortage of memory safe languages that you can easily use from C/C++ programs. Nobody is going to ship a JVM with their project just so they can have the "fun" experience of using Java FFI to do crypto.

Realistically Rust is still the only memory safe language that you could use, so it's not especially surprising that nobody did it 18 years ago.

> The issue isn't really that there was a shortage of memory safe languages, it's that there was a shortage of memory safe languages that you can easily use from C/C++ programs.

Just as importantly, there was also a shortage of memory safe languages that had good performance.

AFAIK the issue with messaging isn't that the core app itself is written in an unsafe language , but that many components it interacts with are unsafe. E.g file format parsers using standard libraries to do it.

Granted those should also be rewritten in safer languages but often they're massive undertakings

In 2003 you could have generated C from a DSL, for one. Like yacc and lex had been standard practice (although without security focus) since the 80s.

Or generate C from a safe GP language, eg C targeting Scheme such as Chicken Scheme / Bigloo / Gambit.

People have been shipping software in memory safe languages all this time, since way before stack smashing was popularized in Phrack, after all.

I think a good approach could be what curl is doing. AFAIK they are replacing some security-critical parts of their core code with Rust codebases, importantly without changing the API.

Modula-2 (1978), Object Pascal (1980), .....

My language selection checklist:

1. Does the program need to be fast or complicated? If so, don't use a scripting language like Python, Bash, or Javascript.

2. Does the program handle untrusted input data? If so, don't use a memory-unsafe language like C or C++.

3. Does the program need to accomplish a task in a deterministic amount of time or with tight memory requirements? If so, don't use anything with a garbage collector, like Go or Java.

4. Is there anything left besides Rust?

By 'complicated' in point 1, do you mean 'large'? Because a complex algorithm should be fine -- heck, it should be better in something like Python because it's relatively easy to write, so you have an easier time thinking about what you're doing, avoid making a mistake that would lead to an O(n³) runtime instead of the one you were going for, takes less development time, etc.

I assume you meant 'large' because, as software like Wordpress beautifully demonstrates, you can have the simplest program (from a user's perspective) in the fastest language but by using a billion function calls for the default page in a default installation, you can make anything slow. Using a slow language for large software, if that's what you meant to avoid then I agree.

And as another note, point number 2 basically excludes all meaningful software. Not that I necessarily disagree, but it's a bit on the heavy-handed side.

By complicated I guess I mean "lots of types". Static typing makes up for its cost once I can't keep all the types in my head at the same time.

Point number 2 excludes pretty much all network-connected software, and that's intentional. I suppose single-player games are ok to write in C or C++.

> 2 excludes pretty much all network-connected software

Not caring about local privilege escalation I see ;). Attack surface might be lower, but from lockscreens to terminals there is a lot of stuff out there that doesn't need to be connected to the internet itself before I find it quite relevant to consider whether it was written in a dumb language.

I suspect Ada would make the cut, with the number of times it's been referenced in these contexts, but I haven't actually taken the time to learn Ada properly. It seems like a language before its time.

As I understand it it's only memory safe if you never free your allocations, which is better than C but not an especially high bar. Basically the same as GC'd languages but without actually running the GC.

It does have support for formal verification though unlike most languages.

> if you never free your allocations

Technically, it used to be memory safe before this and they rolled back the restrictions to allow "unchecked deallocation".

Pointers ("Accesses") are also typed, e.g. you can have two incompatible flavors of "Widget*" which can't get exchanged which helps reduce errors, and pointers can only point at their type of pointer unless specified otherwise, are null'd out on free automatically and checked at runtime. In practice, you just wrap your allocations/deallocations in a smart pointer or management type, and any unsafe usages can be found in code by looking for "Unchecked" whether "Unchecked_Access" (escaping an access check) or "Unchecked_Deallocation".

The story is quite different in Ada because of access type checks, it doesn't use null-terminated strings, it uses bounded arrays, has protected types for ensuring exclusive access and the language implicitly passes by reference when needed or directed.

My experience with writing concurrent Ada code has been extremely positive and I'd highly recommend it.

Ada has improved a lot since Ada 83, it is quite easy to use RAII since Ada 2005.

Re 3, people have known how to build real-time GCs since like the 70s and 80s. Lots of Lisp systems were built to handle real-time embedded systems with a lot less memory that our equivalent-environment ones have today. Even Java was originally built for embedded. While it's curious that mainstream GC implementations don't tend to include real-time versions (and for harder guarantees need to have all their primitives documented with how long they'll execute for as a function of their input, which I don't think Rust has), it might be worth it to schedule 3-6 months of your project's planning to make such a GC for your language of choice if you need it. If you need to be hard real time though, as opposed to soft, you're likely in for a lot of work regardless of what you do. And you're not likely going to be building a mass-market application like a browser on top of various mass-market OSes like Windows, Mac, etc.

If your "deterministic amount of time" can tolerate single-digit microsecond pauses, then Go's GC is just fine. If you're building hard real time systems then you probably want to steer clear of GCs. Also, "developer velocity" is an important criteria for a lot of shops, and in my opinion that rules out Rust, C, C++, and every dynamically typed language I've ever used (of course, this is all relative, but in my experience, those languages are an order of magnitude "slower" than Go, et al with respect to velocity for a wide variety of reasons).

My impression was Go's GC was a heck of a lot slower than "single-digit microsecond pauses." I would love a source on your claim

I had seen some benchmarks several years ago around the time when the significant GC optimizations had been made, and I could've sworn they were on the order of single-digit microseconds; however, I can't find any of those benchmarks today and indeed any benchmarks are hard to come by except for some pathological cases with enormous heaps. Maybe that single-digit  µs values was a misremembering on my part. Even if it's sub-millisecond that's plenty for a high 60Hz video game.

If it can really guarantee single-digit microsecond pauses in my realtime thread no matter what happens in other threads of my application, that is indeed a game changer. But I'll believe it when I see it with my own eyes. I've never even used a garbage collector that can guarantee single-digit millisecond pauses.

Have you measured the pause times of free()? Because they are not deterministic, and I have met few people who understand in detail how complex it can be in practice. In the limit, free() can be as bad as GC pause times because of chained deallocation--i.e. not statically bounded.

People don't call free from their realtime threads.

This is true, but for performance's sake, you should not alloc/free in a busy loop, especially not on a real time system.

Allocate in advance, reuse allocated memory.

> Allocate in advance, reuse allocated memory.

In practice, almost all real-time systems use this strategy, even going so far as to allocate all memory at compile time. The first versions of Virgil (I and II) used this strategy and compiled to C.

When doing this, the whole debate of memory safe (automatic) vs unsafe (manual) is completely orthogonal.

But you can generally control when free is called.

Not sure current state of the art, but Go's worst-case pause time five years ago was 100µs: https://groups.google.com/g/golang-dev/c/Ab1sFeoZg_8

Discord was consistently seeing pauses in the range of several hundred ms every 2 minutes a couple years ago.


Hard to say without more details, but those graphs look very similar to nproc numbers of goroutines interacting with the Linux-of-the-time's CFS CPU scheduler. I've seen significant to entire improvement to latency graphs simply by setting GOMAXPROC to account for the CFS behavior. Unfortunately the blog post doesn't even make a passing mention to this.

Anecdotally, the main slowdown we saw of Go code running in Kubernetes at my previous job was not "GC stalls", but "CFS throttling". By default[1], the runtime will set GOMACSPROCS to the number of cores on the machine, not the CPU allocation for the cgroup that the container runs in. When you hand out 1 core, on a 96-core machine, bad things happen. Well, you end up with a non-smooth progress. Setting GOMACPROCS to ceil(cpu allocation) alleviated a LOT of problems

Similar problems with certain versions of Java and C#[1]. Java was exacerbated by a tendency for Java to make everything wake up in certain situations, so you could get to a point where the runtime was dominated by CFS throttling, with occasional work being done.

I did some experiments with a roughly 100 Hz increment of a prometheus counter metric, and with a GOMAXPROCS of 1, the rate was steady at ~100 Hz down to a CPU allocation of about 520 millicores, then dropping off (~80 Hz down to about 410 millicores, ~60 hz down to about 305 millicores, then I stopped doing test runs).

[1] This MAY have changed, this was a while and multiple versions of the compiler/runtime ago. I know that C# had a runtime release sometime in 2020 that should've improved things and I think Java now also does the right thing when in a cgroup.

AFAIK, it hasn't changed, this exact situation with cgroups is still something I have to tell fellow developers about. Some of them have started using [automaxprocs] to automatically detect and set.

[automaxprocs]: https://github.com/uber-go/automaxprocs

Ah, note, said program also had one goroutine trying the stupidest-possible way of finidng primes in one goroutine (then not actyakly doing anything with the found primes, apart from appending them to a slice). It literally trial-divided (well, modded) all numbers between 2 and isqrt(n) to see if it was a multiple. Not designed to be clever, explicitly designed to suck about one core.

I found this go.dev blog entry from 2018. It looks like the average pause time they were able to achieve was significantly less than 1ms back then.

"The SLO whisper number here is around 100-200 microseconds and we will push towards that. If you see anything over a couple hundred microseconds then we really want to talk to you.."


I believe Java’s ZGC has max pause times of a few milliseconds

Shenandoah is in the the same latency category as well. I haven't seen recent numbers but a few years ago it was a little better latency but a little worse throughput.

3b. Does your program need more than 100Mb of memory?

If no, then just use a GC'd language and preallocate everything and use object pooling. You won't have GC pauses because if you don't dynamically allocate memory, you don't need to GC anything. And don't laugh. Pretty much all realtime systems, especially the hardest of the hard real time systems, preallocate everything.

> My language selection checklist:

1. What are the people going to implement this an expert in?

Choose that. Nothing else matters.

pretty sure the people who wrote the vulnerable code were experts.

Answering a question with a sincere question: if the answer to 3 is yes to deterministic time, but no to tight memory constraints, does Swift become viable in question 4? I suspect it does, but I don’t know nearly enough about the space to say so with much certainty.

I'm not super familiar with Swift, but I don't see how it could be memory-safe in a multi-threaded context without some sort of borrow checker or gc. So I think it is rejected by question #2.

Swift uses automatic reference counting. From some cursory reading, the major difference from Rust in this regard is that Swift references are always tracked atomically, whereas in Rust they may not be atomic in a single-owner context.

To my mind (again, with admittedly limited familiarity), I would think:

- Atomic operations in general don’t necessarily provide deterministic timing, but I'm assuming (maybe wrongly?) for Rust’s case they’re regarded as a relatively fixed overhead?

- That would seem to hold for Swift as well, just… with more overhead.

To the extent any of this is wrong or missing some nuance, I’m happy to be corrected.

Incrementing an atomic counter every time a reference is copied is a significant amount of overhead, which is why most runtimes prefer garbage collection to reference counting (that, and the inability of referencing counting to handle cycles elegantly).

Rust doesn't rely on reference counting unless explicitly used by the program, and even then you can choose between atomically-reference-counted pointers (Arc) vs non-atomic-reference-counted pointers (Rc) that the type system prevents from being shared between threads.

I promise I’m not trying to be obtuse or argumentative, but I think apart from cycles your response restates exactly what I took from my reading on the subject and tried to articulate. So I’m not sure if what I should take away is:

- ARC is generally avoided by GC languages, which puts Swift in a peculiar position for a language without manual memory management (without any consideration of Swift per se for the case I asked about)

- Swift’s atomic reference counting qualitatively eliminates it from consideration because it’s applied even in single threaded workloads, negating determinism in a way I haven’t understood

- It’s quantitatively eliminated because that overhead has such a performance impact that it’s not worth considering

Swift has a similar memory model to Rust, except that where Rust forbids things Swift automatically copies them to make it work.

People using other languages appear terrified of reference count slowness for some reason, but it usually works well, and atomics are fast on ARM anyway.

It's important to note that while Swift often allows for code similar to Rust to be written, the fact that it silently inserts ARC traffic or copies often means that people are going to write code that do things that Rust won't let them do and realize after the fact that their bottleneck is something that they would never have written in Rust. I wouldn't necessarily call this a language failure, but it's something worth looking out for: idiomatic Swift code often diverges from what might be optimally efficient from a memory management perspective.

To provide some context for my answer, I’ve seen, first hand, plenty of insecure code written in python, JavaScript and ruby, and a metric ton - measured in low vulnerabilities/M LoC - of secure code written in C for code dating from the 80s to 2021.

I personally don’t like the mental burden of dealing with C any more and I did it for 20+ years, but the real problem with vulnerabilities in code once the low hanging fruit is gone is the developer quality, and that problem is not going away with language selection (and in some cases, the pool of developers attached to some languages averages much worse).

Would I ever use C again? No, of course not. I’d use Go or Rust for exactly the reason you give. But to be real about it, that’s solving just the bottom most layer.

C vulnerabilities do have a nasty habit of giving the attacker full code execution though, which doesn’t tend to be nearly so much of a problem in other languages (and would likely be even less so if they weren’t dependant on foundations written in C)

I don’t disagree with you. But just before writing that message I was code reviewing some python that was vulnerable a to the most basic SQL injection. Who needs or wants execution authority when you can dump the users table of salted sha passwords?

In C you can just use code execution to grab the passwords before they get salted…

One is the whole database.

I am very aware of code execution attacks and persistence around them. C has a class of failures that are not present in most other languages that are in current use, my point is that it really only solves part of the problem.

From a security perspective, the 90% issue is the poor quality of developers in whatever language you choose.

I think the difference is in the difficulty in preventing the bugs. I'd be very surprised if our codebase at work contained SQL injection bugs. We use a library that protects against them by default, and all code gets reviewed by a senior developer. SQL injection is easy to prevent with a simple code review process.

Subtle issues with undefined behaviour, buffer overflows, etc in C are much trickier and frequently catch out even highly experienced programmers. Even with high quality developers your C program is unlikely to be secure.

SQL or shell command injection also gives attackers full code execution.

> or something crazy like rust?

There's nothing crazy about rust.

If your example was ATS we'd be talking.

It’s not lost on me that the organization that produced NSS also invented Rust. That implies the knowledge of this need is there, but it’s not so straightforward to do.

> Or are C/C++ benefits just too high to ever give up?

FFI is inherently memory-unsafe. You get to rewrite security critical things from scratch, or accept some potentially memory-unsafe surface area for your security critical things for the benefit that the implementation behind it is sound.

This is true even for memory-safe languages like Rust.

The way around this is through process isolation and serializing/deserializing data manually instead of exchanging pointers across some FFI boundary. But this has non-negligible performance and maintenance costs.

> FFI is inherently memory-unsafe

Maybe this specific problem needs attention. I wonder, is there a way we can make FFI safer while minimizing overhead? It'd be nice if an OS or userspace program could somehow verify or guarantee the soundness of function calls without doing it every time.

If we moved to a model where everything was compiled AOT or JIT locally, couldn't that local system determine soundness from the code, provided we use things like Rust or languages with automatic memory management?

Presumably as long as FFIs are based on C calling conventions and running native instructions it would be unsafe. You could imagine cleaner FFIs that have significant restrictions placed on them (I'd imagine sandboxing would be required) but if the goal is to have it operate with as little overhead as possible, then the current regime is basically what you end up with and it would be decidedly unsafe.

This is a really hard problem because you have to discard the magic wand of a compiler and look at what is really happening under the hood.

At its most rudimentary level, a "memory safe" program is one that does not access memory that is forbidden to it at any point during execution. Memory safety can be achieved using managed languages or subsets[1] of languages like Rust - but that only works if the language implementations have total knowledge of memory accesses that the program may perform.

The trouble with FFIs is that by definition, the language implementations cannot know anything about memory accesses on the other side of the interface - it is foreign, a black box. The interface/ABI does not provide details about who is responsible for managing this memory, whether it is mutable or not, if it is safe to be reused in different threads, indeed even what the memory "points to."

On top of that, most of the time it's on the programmer to express to the implementation what the FFI even is. It's possible to get it wrong without actually breaking the program. You can do things like ignore signedness and discard mutability restrictions on pointers with ease in an FFI binding. There's nothing a language can do to prevent that, since the foreign code is a black box.

Now there are some exceptions to this, the most common probably being COM interfaces defined by IDL files, which are language agnostic and slightly safer than raw FFI over C functions. In this model, languages can provide some guarantees over what is happening across FFI and which operations are allowed (namely, reference counting semantics).

The way around all of this is simple : don't share memory between programs in different languages. Serialize to a data structure, call the foreign code in an isolated process, and only allow primitive data (like file descriptors) across FFI bounds.

This places enormous burden on language implementers, fwiw, which is probably why no one does it. FFI is too useful, and it's simple to tell a programmer not to screw up than reimplement POSIX in your language's core subroutines.

[1] "safe" rust, vs "unsafe" where memory unsafety is a promise upheld by the programmer, and violations may occur

Is there a way to do FFI without having languages directly mutate each other's memory, but still within the same process. So all 'communication' between the languages happens by serializing data, no shared memory being used for FFI. But you don't get the massive overhead of having to launch a second process.

You are still depending on the called function not clobbering over random memory. But if the called function is well-behaved you would have a clean FFI.

In practice you run all your process-isolated code in one process as a daemon instead of spawning it per call.

I think you're on the right track in boiling down the problem to "minimize the memory shared between languages and restrict the interface to well defined semantics around serialized data" - which in modern parlance is called a "remote procedure call" protocol (examples are gRPC and JSON RPC).

It's interesting to think about how one could do an RPC call without the "remote" part - keep it all in the same process. What you could do is have a program with an API like this :

    int rpc_call (int fd); 
where `fd` is the file descriptor (could even be an anonymous mmap) containing the serialized function call, and the caller gets the return value by reading the result back.

One tricky bit is thread safety, so you'd need a thread local file for the RPC i/o.

Writing a small wrapper that enforces whatever invariants are needed at the FFI boundary is much, much easier to do correctly than writing a whole program correctly.

You are never going to get 100% memory safety in any program written in any language, because ultimately you are depending on someone to have written your compiler correctly, but you can get much closer than we are now with C/C++.

C/C++ don’t really have “benefits”, they have inertia. In a hypothetical world where both came into being at the same time as modern languages no one would use them.

Sadly, I’m to the point that I think a lot of people are going to have to die off before C/C++ are fully replaced if ever. It’s just too ingrained in the current status quo, and we all have to suffer for it.

On any given platform, C tends to have the only lingua franka ABI. For that reason it will be around until the sun burns out.

The C ABI will outlive C, like the term "lingua franca" outlived the Franks. Pretty much every other language has support for the C ABI.

One complication is that it's not just about ABIs but at least as much about APIs. And C headers often make some use of the preprocessor. Usage of the preprocessor often even is part of the API, i.e. APIs expose preprocessor macros for the API consumer.

Zig has a built-in C compiler and supposedly you can just "include <header.h>" from within a Zig source file. Rust has a tool called bindgen. There are other tools, I haven't tried either of them, but the fact alone that I'm somewhat familiar with the Windows (and some other platforms') headers makes me not look forward to the friction of interfacing with some tried and true software platforms and libraries from within a different language.

I know there has been some work going on at Windows on porting their APIs to a meta-language. Does anyone know how much progress was made on that front?

The Windows meta-API thing is done: https://lib.rs/windows

We'll probably continue using C headers as a poor ABI definition format, even without writing programs in C. Sort-of like JSON used outside of JavaScript, or M4 known as autoconf syntax, rather than a standalone language.

But C headers as an ABI definition format are overcomplicated and fragile (e.g. dependent on system headers, compiler-specific built-in defs, and arbitrary contraptions that make config.h), and not expressive enough at the same time (e.g. lacking thread-safety information, machine-readable memory management, explicit feature flags, versioning, etc.).

So I think there's a motivation to come up with a better ABI and a better ABI definition format. For example, Swift has a stable ABI that C can't use, so there's already a chunk of macOS and iOS that has moved past C.

Not disagreeing about the issues of header files and the difficulty of consuming them from other languages (which was my point).

But regarding ABI definitions, I suspect that introducing "thread-safety information, machine-readable memory management, explicit feature flags" will make interopability at the binary level difficult or impossible, which is even worse.

Why do you think so? These things don't define binary layout, so they shouldn't interfere with it. In the worst case the extra information can be ignored entirely, and you'll have the status quo of poor thread safety, fragility of manual memory management, and crashes when the header version is different than binary .so version (the last point is super annoying. Even when the OS can version .so libs, C gives you absolutely no guarantee that the header include path and defines given to the compiler are compatible with lib include paths given to the linker).

On Android, ChromeOS, IBM i, z/OS, ClearPath it isn't.

C/C++ will be around for at least a hundred years. Our descendants will be writing C/C++ code on Mars.

I don’t know about that, I can see Rust having a certain aesthetic appeal to martians.

Nah, they definitely use Zig.

Why not write it in a language not written yet?

As I replied elsewhere.

You don't need to explain to Mozilla about rewriting code from C/C++ to Rust.

this is C code. stuff like void*, union and raw arrays do not belond in modern C++. while C++ is compatible with C it provides ways to write safer code that C doesn't. writing C code in a C++ project is similar to writing inline assembly.

Well, there's no time machine.

Also, as far as I know, a full replacement for C doesn't exists yet.

Are you suggesting that this crypto library would not be possible or practical to be built with rust? What features of C enable this library which Rust does not?

There is no time machine to bring rust back to when this was created, but as far as I know, there is no reason it shouldn't be Rust if it was made today.

It's more of whether Rust fits into every workflow, project, team, build chain, executable environment, etc., that C does. Does rust run everywhere C runs? Does rust build everywhere C builds? Can rust fit into every workflow C does? Are there rust programmers with all the same domain expertise as for C programmers?

(Not to mention, the question here isn't whether to write in rust or write in C. It's whether to leave the C code as-is -- zero immediate cost/time/attention or rewrite in rust -- a significant up-front effort with potential long term gains but also significant risk.)

Rust does not run everywhere C runs. At least not yet - there's a couple efforts to allow rust to compile to all platforms GCC supports[1]. But we don't need rust to work everywhere C works to get value out of a native rust port of OpenSSL. Firefox and Chrome (as far as I know) only support platforms which have rust support already.

As I said in another comment, in my experience, porting code directly between two C-like languages is often much faster & cheaper than people assume. You don't have to re-write anything from scratch; just faithfully port each line of code in each method and struct across; with some translation to make the new code idiomatic. 1kloc / day is a reasonable ballpark figure, landing us at about one person-year to port boringssl to rust.

The downside of this is that we'd probably end up with a few new bugs creeping in. My biggest fear with this sort of work is that the fuzzers & static analysis tools might not be mature enough in rust to find all the bugs they catch in C.

[1] https://lwn.net/Articles/871283/

Speaking as a static analysis person, C and C++ are unholy nightmares for static analysis tools to work with. Even very very basic issues like separate compilation make a mess of things. If the world can successfully shift away from these languages, the supporting tools won't have trouble keeping up.

>"1kloc / day is a reasonable ballpark figure, landing us at about one person-year to port boringssl to rust."

Porting 1k lines a day, testing and catching and fixing errors to language with incompatible memory model and doing it for a year is insanity. Programmers who propose this kind of productivity most likely have no idea about real world.

There’s an old saying attributed to Abe Lincoln: “Give me six hours to chop down a tree and I will spend the first four sharpening the axe.”

I contend that most software engineers (and most engineering houses) don’t spend any time sharpening the axe. Certainly not when it comes to optimizing for development speed. Nobody even talks about deliberate practice in software. Isn’t that weird?

1000 lines of code is way too much code to write from scratch in a day, or code review. I claim it’s not a lot of code to port - especially once you’re familiar with the code base and you’ve built some momentum. The hardest part would be mapping C pointers into rust lifetimes.

> most likely have no idea about real world.

I have no doubt this sort of speed is unheard of in your office, with your processes. That doesn’t tell us anything about the upper bound of porting speed. If you have more real world experience than me, how fast did you port code last time you did it?

Mind you, going hell to leather is probably a terrible idea with something as security sensitive as BoringSSL. It’s not the project to skip code review. And code review would probably take as long again as porting the code itself.

I'm also really skeptical that one could maintain 1K/lines per day for more than a couple weeks if that.

There have been a lot of studies that measure average output of new code at only ~15 or so LOC/day. One can manage more on small projects for a short amount of time.

I could believe porting between two C-like languages is 1 order of magnitude easier, but not 2. Std library differences, porting idioms, it adds up.

Even just reading and really understanding 1K lines/day is a lot.

I'd love to see some data about that. Even anecdotal experiences - I can't be the only one here who's ported code between languages.

I agree with that LOC/day output figure for new code. I've been averaging somewhere around that (total) speed for the last few months - once you discard testing code and the code I'll end up deleting. Its slower than I usually code, but I'm writing some very deep, new algorithmic code. So I'm not beating myself up about it.

But porting is very different. You don't need to think about how the code works, or really how to structure the data structures or the program. You don't need much big picture reasoning at all, beyond being aware of how the data structures themselves map to the target language. Most of the work is mechanical. "This array is used as a vec, so it'll become Vec<>. This union maps to an enum. This destructor should actually work like this...". And for something like boringSSL I suspect you could re-expose a rust implementation via a C API and then reuse most of the BoringSSL test suite as-is. Debugging and fuzz testing is also much easier - since you just need to trace along the diff between two implementations until you find a point of divergence. If the library has tests, you can port them across in the same way.

The only data point I have for porting speed is from when I ported chipmunk2d in a month from C to JS. Chipmunk only has a few data structures and it was mostly written by one person. So understanding the code at a high level was reasonably easy. My ported-lines-per-day metric increased dramatically throughout the process as I got used to chipmunk, and as I developed norms around how I wanted to translate different idioms.

I have no idea how that porting speed would translate to a larger project, or with Rust as a target language. As I said, I'd love to hear some stories if people have them. I can't be the only one who's tried this.

> I have no idea how that porting speed would translate to a larger project

IME it doesn't.

I'm not at liberty to discuss all that much detail, but I what I can say is: This was a mixture of C and C++ -> Scala. This was pretty ancient code which used goto fairly liberally, etc. so it would often require quite a lot of control flow rewriting -- that take a looooong time. I'd be lucky to get through a single moderately complex multiply nested loop per day. (Scala may be a bit of an outlier here because it doesn't offer a c-like for loop, nor does it offer a "native" break statement.)

I've ported / developed from scratch way more than 1000 a day on emergency basis. Doing it reliably and with proper testing every day for a year - thanks, but no thanks.

>"I have no doubt this sort of speed is unheard of in your office, with your processes."

I am my own boss for the last 20+ years and I do not have predefined processes. And I am not in a dick size contest.

Most fuzzers I’m aware of work with Rust. You can use the same sanitizers as well.

Static analysis means a wide range of things, and so some do and some don’t work with Rust. I would be very interested to learn about C static analysis that somehow wouldn’t work with Rust at all; it should be easier to do so in rust because there’s generally so much more information available already thanks to the language semantics.

Eg: the borrow checker is a kind of static analysis that's quite difficult to do soundly for C/C++ (and most other languages)

It only matters if rust runs everywhere that Firefox runs, which it does.

Considering firefox already uses rust components, that seems a safe bet.

I'd like to write go or rust but embedded constraints are tough. I tried and the binaries are just too big!

How big is too big? I haven't run into any size issues writing very unoptimized Go targeting STM32F4 and RP2040 microcontrollers, but they do have a ton of flash. And for that, you use tinygo and not regular go, which is technically a slightly different language. (For some perspective, I wanted to make some aspect of the display better, and the strconv was the easiest way to do it. That is like 6k of flash! An unabashed luxury. But still totally fine, I have megabytes of flash. I also have the time zone database in there, for time.Format(time.RFC3339). Again, nobody does that shit on microcontrollers, except for me. And I'm loving it!)

Full disclosure, Python also runs fine on these microcontrollers, but I have pretty easily run out of RAM on every complicated Python project I've done targeting a microcontroller. It's nice to see if some sensor works or whatever, but for production, Go is a nice sweet spot.

I have 16MB of flash and I wanted to link in some webrtc Go library and the binary was over 1MB. As I had other stuff it seemed like C was smaller.

I took a look at using github.com/pion/webrtc/v3 with tinygo, but it apparently depends on encoding/gob which depends on reflection features that tinygo doesn't implement. No idea why they need that, but that's the sort of blocker that you'll run into.

The smallest binary rustc has produced is 138 bytes.

It is true that it’s not something you just get for free, you have to avoid certain techniques, etc. But rust can fit just fine.

Do you have a link to an article / report about that 138 byte program? I'd be interested how to achieve that.

https://github.com/tormol/tiny-rust-executable got it to 137, but here's a blog post explaining the process to get it to 151: http://mainisusuallyafunction.blogspot.com/2015/01/151-byte-...

I'd also like to see the smallest Rust binaries that are achieved by real projects. When the most size-conscious users use Rust to solve real problems, what is the result?

Our embedded OS, Hubris, released yesterday, with an application as well, the entire image is like, 70k? I’ll double check when I’m at a computer.

Yeah, so, using arm-none-eabi-size, one of these apps I have built, the total image size is 74376 bytes. The kernel itself is 29232 of those, the rest are various tasks. RCC driver is 4616, USART driver is 6228.

We have to care about binary size, but we also don't have to stress about it. There are some things we could do to make sizes get even smaller, but there's no reason to go to that effort at the moment. We have the luxury of being on full 32-bit environments, so while there isn't a ton of space by say, desktop or phone standards, it's also a bit bigger than something like a PIC.

Lots of folks also seem to get the impression that Rust binary sizes are bigger than they are because they use ls instead of size; we care deeply about debugging, and so the individual tasks are built with debug information. ls reports that rcc driver is 181kb, but that's not representative of what actually ends up being flashed. You can see a similar effect with desktop Rust software by running strip on the binaries, you'll often save a ton of size from doing that.

This is not true. Lots of people are putting Rust on microcontrollers now - just have to stick to no_std.

I wondered this when I recently saw that flatpak is written in C. In particular for newer projects that don't have any insane performance constraints I wonder why people still stick to non memory-managed languages.

Dumb question: Do we need to use C++ anymore? Can we just leave it to die with video games? How many more years of this crap do we need before we stop using that language. Yes I know, C++ gurus are smart, but, you are GOING to mess up memory management. You are GOING to inject security issues with c/c++.

If im going to be making code that needs to run fast, works on a bit level, and isn't exposed to the world, then I am picking up C++

It's more convenient than C. It's easier to use (at the cost of safety) compared to Rust.

Perhaps this will change if I know rust better. But for now C++ is where it's at for me for this niche.

If you drop the parts of C++ that are that way because of C it is a much safer language. Weird and inconsistent, but someone who is writing C++ wouldn't make the error in the code in question any more than they would in rust. In C++ we never is unbounded arrays, just vectors and other bounded data structures.

I often see students asking a C++ question and when I tell then that is wrong they respond that their professor has banned vector. We have a real problem with bad teachers in C++, too many people learn to write C that builds with a C++ compiler and once in a while has a class.

C/C++ is great for AI/ML/Scientific computing because at the end of the day, you have tons of extremely optimized libraries for "doing X". But the thing is, in those use cases your data is "trusted" and not publicly accessible.

Similarly in trading, C/C++ abounds since you really do have such fine manual control. But again, you're talking about usage within internal networks rather than publicly accessible services.

For web applications, communications, etc.? I expect we'll see things slowly switch to something like Rust. The issue is getting the inertia to have Rust available to various embedded platforms, etc.

I'm a big proponent of rust, but I doubt rust will displace nodejs & python for web applications.

Web applications generally care much more about developer onboarding and productivity than performance. And for good reason - nodejs behind nginx is fast enough for the bottom 99% of websites. Rust is much harder to learn, and even once you've learned it, its more difficult to implement the same program in rust than it is in javascript. Especially if that program relies heavily on callbacks or async/await. For all the hype, I don't see rust taking off amongst application developers.

Its a great language for infrastructure & libraries though. This bug almost certainly wouldn't have existed in rust.



I read "double free", "denial of service", "out-of bounds read", "NULL pointer dereference", etc...

And that's a list of vulnerabilities found for a language that is barely used compared to C/C++ (in the real world).

It won't change. C/C++ dominates and will dominate for a very long time.

You need to read the list more carefully.

• The list is not for Rust itself, but every program ever written in Rust. By itself it doesn't mean much, unless you compare prevalence of issues among Rust programs to prevalence of issues among C programs. Rust doesn't promise to be bug-free, but merely catch certain classes of bugs in programs that don't opt out of that. And it delivers: see how memory unsafety is rare compared to assertions and uncaught exceptions: https://github.com/rust-fuzz/trophy-case

• Many of the memory-unsafety issues are on the C FFI boundary, which is unsafe due to C lacking expressiveness about memory ownership of its APIs (i.e. it shows how dangerous is to program where you don't have the Rust borrow checker checking your code).

• Many bugs about missing Send/Sync or evil trait implementations are about type-system loopholes that prevented compiler from catching code that was already buggy. C doesn't have these guarantees in the first place, so in C you have these weaknesses all the time by design, rather than in exceptional situations.

> The list is not for Rust itself, but every program ever written in Rust.

This is, I think, obvious unless you are talking about C/C++ compiler bugs (which I am not).

But if you think it that way, the same happens with C/C++! Besides compiler bugs, the published CVEs for C/C++ "are not for C/C++ itself, but every program ever written in C/C++".

Still, potential severe vulnerabilities in Rust stdlib can happen too (https://www.cvedetails.com/cve/CVE-2021-31162/ or https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-3631...), so there is a blurry limit between "a bug is in Rust" or not.

> unless you compare prevalence of issues among Rust programs to prevalence of issues among C programs.

I don't know but you mention an interesting PoV. Do you propose to compare the ratio of total quantity of code EVER written in C/C++ and the numbers of "issues" found? Compared to the quantity of "issues" per code EVER written in Rust?

I guess nobody can figure out those numbers, but if I have to guess, then the ratio of total "Quantity of code"/"issues" will be in favor of C/C++.

I don't want to discuss the cause/reason of those Rust vulnerabilities, but the message I read is: "unaware programmers can still introduce potential UAF, double-free, read uninitialized memory, dereference NULL pointers, etc. Just like with C/C++ but just in smaller quantities".

Mitre doesn't tag every program written in C with the C tag.

A few bugs in std happened, but they're also mostly in edge-case situations that in C/C++ would be either straight-up UB or "you're bad for even trying this", like integer overflow, throwing an exception from a destructor, or implementing operator overloading that gives randomized results.

It's not a tag but a keyword search. I don't know projects entirely written in Rust (other than single crates) to look for (by name). So probably there are more Rust-based vulnerabilities around than the ones from the Mitre query from the link.

And yes, edge-cases are the worst. The only concern is that these vulnerabilities were introduced by people who know the language more than anyone (I'd like to think that patches mainlined into std are written, revised and maintained by the best Rust developers). I'm afraid to imagine what kind of vulnerabilities could a person like me introduce in my own Rust programs.

That's the point of splitting Rust into safe and unsafe. If you're not trusting yourself with C-like danger, then stick to writing safe Rust. When you need to do something risky, then it will require unsafe{} blocks, which are a signal to be extra careful, and stand out in code reviews.

Also keep in mind that std is in unusual position, because it provides the unsafe foundation for safe programs. For example, you as a Rust user can't cause any memory unsafety when using the String type, but std had to define it for you from unsafe primitives. This concept of building safe abstraction from unsafe primitives is similar to Python: CPython is written in an unsafe language, but this unsafety is hidden from Python programs.

>"Dumb question..."


C++ is awesome and fast, don't blame it for human error.

when "human error" happens at a much higher rate than the alternatives, it's fair to blame it.

FWIW, Go absolutely would not stop you writing unbounded data into a bounded struct. Idiomatic Go would be to use byte slices which auto-resize, unlike idiomatic C, but you still have to do it.

Go would stop this from being exploitable. You might be able to make a slice larger than it is "supposed" to be, but it won't overwrite anything else because Go will be allocating new memory for your bigger slice.

But this is hardly a big claim for Go. The reality is that of all current languages in use, only C and C++ will let this mistake happen and have the consequence of overwriting whatever happens to be in the way. Everything else is too memory safe for this to happen.

What's the exploit path assuming no use of unsafe?

I can see situations where I could probably get go to crash, but not sure how I get go to act badly.

Note: Not a go / Haskell / C# expert so understanding is light here.

Go is sometimes considered memory unsafe because of the presence of data races. (This is a controversial semantics.)

Even in the case of data races, you could not develop an exploit like the one discussed in this blog post, right? It's kinda a non-sequitur in this context?

Go allows data races to arbitrarily corrupt metadata, which is the precursor to an exploit like this. A brief rule-of-thumb is if the race allows you to directly touch data that isn't available via APIs, such as the count on a vector–once you do that, you can "resize" it to be larger than its actual size, and run off the end to do whatever you want. (There are many other ways to achieve something similar: type confusion, etc.)

Then Java is also unsafe by the same standard.

Why do you say that? Go's data races can produce memory corruption through bounds checking failures. I'm not aware of Java having that kind of memory corruption.

Yes. Java and C# are unsafe by the same standard. Last I checked, they both allow multiple threads to nonatomically modify a raw variable - which can lead to data races and data corruption.

I haven't heard of anyone getting RCE out of this class of bug in C# or java though.

JVM semantics[1] don't allow this to corrupt program state arbitrarily, just the data variables in question, right? Whereas in Go.. Search for "corruption" in https://research.swtch.com/gomm

[1] Not just Java, meaning all the many nice JVM languages enjoy this is as well, eg Clojure, Scala etc

It's common to exploit JavaScript engines with this kind of bug, and JS engines probably have better security teams at this point, so I expect you could get an RCE if you really tried.

Yes, but those would be bugs in the runtime itself, rather than in the programming language. Java and JavaScript both define behavior of racy code; Go does not.

Go has no "unsafe" keyword and several parts of the language are unsafe, you're thinking of Rust which has much tighter guarantees.

Go idioms, like accepting data into buffers that are resized by "append", work around the unsafe parts of the language.

Go has an unsafe package.

Is there an example of even "bad" go code that gets you from a overflow to an exploit? I'm curious, folks (usually rust folks) do keep making this claim, is there a quick example?

You can totally do this with bad concurrency in Go: read-after-write of an interface value may cause an arbitrarily bad virtual method call, which is somewhat UB. I am not aware of single goroutone exploits, though.

Concurrency issues / bad program flow feel a bit different don't they? I mean, I can store the action to take on a record in a string in any language, then if I'm not paying attention on concurrency someone else can switch to a different action and then when that record is processed I end up deleting instead of editing etc.

I mention this because in SQL folks not being careful end up in all sorts of messed up situations with high concurrency situations.

It's a different kind of bug–changing the type on a record cannot give you a shell, it can just let you do something funny with the record, such as deleting it. Which is bad, of course, but a bounded bad.

Memory corruption is unbounded bad: in general, corruption is arbitrary code execution. Your program might never interact with the shell but it's going to anyways, because an attacker is going to redirect code execution through the libc in your process. This is just not possible in languages like Java* , which provide details of what kinds of (mis)behaviors are permissible when a race occurs. The list of things is always something like "one of the two writes succeeds" or similar, not "¯\_(ツ)_/¯".

*Barring bugs in the runtime, which do exist…but often because they're written in unsafe languages ;) Although, a bug in runtime written in a safe language will also give you arbitrary code execution…but that's because the job of the code is to enable arbitrary code execution.

Is it idiomatic go to memcpy into a struct? I would think that this whole paradigm would be impossible in safe golang code.

That's what I'm trying to understand.

Let's ignore idiomatic code, people do crazy stuff all the time.

What's the go example that gets you from for example an overflow to exploit? That's what I'm trying to follow (not being an expert).

I am skeptical that you could do it without either using the assembler or the unsafe package, but we will see what Julian says.

Idiomatic go would have you using bounded readers though.

Either you read data into a fixed byte[] and stop at its capacity, or you read data into an unbounded byte[] by using append, and Go looks after the capacity, either way, you can't go off the end.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact