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.
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...
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.
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.
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.
> 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.
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.
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.
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.
I agree that this might slow down the process of porting the code though. I wonder how much by?
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!”
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.
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).
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.
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.
Why? I don't get it. Maintenance of NSS has to be seriously expensive.
Gotta love the US government’s war against crypto.
SSL is complicated because we didn't understand how to design secure protocols in the 90s. Didn't need help.
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:
http://s3.documentcloud.org/documents/784159/sigintenabling-...
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.
It may be too much work to be worth the time, but that's an entirely different matter.
I wonder, how many work years could be too much
(What would you or sbd else guess)
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.
>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.
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?
AFAIK ADA guarantees memory safety only if you statically allocate memory, and other languages have GC overhead.
Rust is really something new.
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.
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.
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.
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,
https://dl.acm.org/doi/10.1145/3274694.3274705
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.
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.
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;
}
}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.
This is where the GC languages did OK.
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.
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,...
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.
Just as importantly, there was also a shortage of memory safe languages that had good performance.
Granted those should also be rewritten in safer languages but often they're massive undertakings
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.
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?
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.
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++.
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.
It does have support for formal verification though unlike most languages.
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.
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.
https://blog.discord.com/why-discord-is-switching-from-go-to...
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.
[automaxprocs]: https://github.com/uber-go/automaxprocs
"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.."
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.
1. What are the people going to implement this an expert in?
Choose that. Nothing else matters.
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.
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.
- 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
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.
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.
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.
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.
There's nothing crazy about rust.
If your example was ATS we'd be talking.
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.
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?
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
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.
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.
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++.
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.
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?
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.
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.
You don't need to explain to Mozilla about rewriting code from C/C++ to Rust.
Also, as far as I know, a full replacement for C doesn't exists yet.
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.
(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.)
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.
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.
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.
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 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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=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.
• 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.
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".
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.
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.
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.
Yep
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.
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.
I haven't heard of anyone getting RCE out of this class of bug in C# or java though.
[1] Not just Java, meaning all the many nice JVM languages enjoy this is as well, eg Clojure, Scala etc
Go idioms, like accepting data into buffers that are resized by "append", work around the unsafe parts of the language.
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?
I mention this because in SQL folks not being careful end up in all sorts of messed up situations with high concurrency situations.
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.
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).
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).