> Fuck Rust and you naive and self-serving evangelists. Come back to me in 20 years with what you've learned.
This might be a cogent criticism (although a pointlessly mean one) if we hadn't started the Rust project with an analysis of precisely what the contents of our security bugs consist of, and designed the project to target those.
Nonsense. How many buffer overflows - a vulnerability that simply doesn't happen in any modern language - would it take to convince you that the language is the problem?
> Languages like Ruby have had their host of ridiculous security errors.
Ruby isn't great, but it's not had anything like the same defect rate as C.
> One advantage of legacy code is that it is battle-tested. It's not sexy, it's not clean, but neither is the real world.
Then why do we keep finding these embarrassingly basic flaws in that "battle-tested" code?
It was literally weeks after Heartbleed. The screams for modern languages with managed runtimes had barely subsided. Then Tomcat, in common use world wide in the poster boy managed language, had a bug with exactly the same impact.
Nobody abandoned Java after that. Just as nobody will abandon C because of this.
> Ruby isn't great, but it's not had anything like the same defect rate
I would argue it's had much worse, altough the comparison is difficult to make. Rails against Apache perhaps, where Rails has had several problems of this magnitude.
I like modern languages as much as the next guy, but I'm very tired of "the language is the problem". Shoving more layers of abstraction in there is not a silver bullet for security. We need to look more at system programming languages.
You're talking about POODLE? Not remotely the same impact. It was a severe vulnerability, but it only leaked plaintext, not key material.
> I like modern languages as much as the next guy, but I'm very tired of "the language is the problem". Shoving more layers of abstraction in there is not a silver bullet for security. We need to look more at system programming languages.
It's not about adding abstraction. There are plenty of safe systems programming languages. The problem is specifically C.
Why would I? That wasn't Tomcat, and it didn't leak keys.
I won't read through the CVE list right now (because it is long, and that's only counting Tomcat and not the JRE itself), but it reused buffers and leaked potentially anything, very much like Heartbleed. If you were in a Java shop at the time you know about it.
> It's not about adding abstraction. There are plenty of safe systems programming languages.
No true Scotsman, eh?
Most people who go about how the world would be saved if we just got rid of C are provably wrong, because popular application in their pet language are just as bad. It used to be Java fanboys, that's why I mentioned Tomcat. Then it was Ruby (and don't get me started on the security disasters Rails brought with it). Most of these do indeed pile abstractions on popular data structures to minimize the possible errors. There are promising languages, but none in popular use, and very far from something you could audit.
False; I was in a Java shop at the time and I don't know about it (though we ran things on Jetty (and later Netty) rather than Tomcat because we felt the code quality was higher).
Since you mention ruby: https://exchange.xforce.ibmcloud.com/vulnerabilities/89191
What was that about not having buffer overflows in "modern" languages?
As for "embarrassingly basic," it takes years or decades to find these bugs because we're using tools that hide them. Basic runtime bounds checking would have turned up this bug pretty much immediately. If you use a language where a buffer overflow often causes no runtime error, then of course it's going to be hard to notice when you've made one.
Don't make me quote Rumsfeld.
There is no single silver bullet, but there exists silver bullets for a variety of problems. When was the last time you saw a pure Python or pure Java code segfaulting from invalid memory accesses? When was the last time you saw a C/C++ code doing the same? It is simply negligent to use a language or system that exposes you to that many more classes of vulnerabilities.
Imagine a language where the compiler randomly accesses another variable if you mistype a name. No error, you just get a random value. Would you defend such a language? "All languages have faults! There is no silver bullet!" Except for this* particular problem, there is a silver bullet.
I agree wholeheartedly with your dismissal of rewriting battle tested code. But the need to migrate is clear, and we don't have to do it at once. It can be done slowly, one piece at a time. And it should be.
* For non-trivial code exposed to untrusted input.
When was the last time you saw Python or pure Java being used to write an interface between applications and kernel?
Those are different languages made for different purposes. They have their own issues (from top of my head pickle module allows to execute code if served with untrusted input), and generally applications written in them are not as wildly deployed as glibc.
Also it's kind of ironic to compare them because both Python and Java are written in C and C++.
Ultimately every language will have bugs when people neglect to validate all possible user input. There is one class of bugs that typically doesn't happen outside C (segfaults) but I can't imagine a C version of Rails executing YAML as code - and at least the segfault is more difficult to exploit.
This is the crux of the matter, so please take a moment to reflect on why you believe that. Ruby code is used in much more dynamic environments; environments that would exist in some other language if Ruby didn't exist, and probably would show the same error.
By choosing C you are not allowing some vulnerabilities to gain resistance against others. The gains are in other places: performance, code inertia. And that's the problem. You are deliberately sacrificing safety. Truth or not, that's why everyone is so pissed at this class of bugs.
- eval/exec() of untrusted turing-complete code
- memory unsafety
There are strange machines and logic vulnerabilities, which as you're aware are pitfalls present in all languages, and can be very subtle. But these are not the same as executing arbitrary operations with all the privileges of the process. This is something we can end today, and Rust would suffice to do it, and can be deployed in many of the same situations as C.
The primary second-system problem with Rust is lack of a stable ABI and upgradable dynamic linking story (e.g. for generics which are effectively inlined even across shared library boundaries), which makes it impossible to upgrade shared libraries. That's a serious problem, which needs to be addressed if Rust is going to improve security for the world outside of the "we static-link 50MB of libraries" Web browser space.
In fact, this is reflected in a common way to expose an interface in this manner: have a main Rust interface (i.e. with normal generics etc.), with thin C-compatible shims that do the right set-up (e.g. converting arguments) to call that code.
Also, I'm not exactly sure how you expect arbitrary cross-crate generics to work for a dynamically reloadable library: they're somewhat inherently incompatible (a downstream type can be inserted deeply into a library's code). I vaguely recall inklings of some approach for C++, beyond just explicitly instantiating specific template parameter combinations in the shared library, so if you know more about that, I'd love to here.
Generics are another place where this becomes the case: if a library exposes a "fold" function generic over both the accumulator and element type, accepting void* instead of T and U means you lose the ability to make sure user don't mix up places where you want an element and places where you want an accumulator.
Dynamically upgradable cross-crate generics, ultimately, require recompilation if you want the performance benefits of monomorphization. But it would be possible to have patchable sites around inlined generics that the dynamic linker could change to instead jump to a version which did not inline generics and which simply called into a non-type-specialized routine in the other library. You'd end up with two versions of compiled code in the binary around each call site for a generic: an inlined-optimized-and-code-motioned one, as usual, and an assumption-free one compiled considering generics to be opaque blobs and using dynamic dispatch for trait methods so that the same code can be used for all instantiations of the generic function.
For example, a function parameterized over an unconstrained type T can't do anything useful with values of that type, except dropping them. Thus code would be generated in which the call to the drop impl would be dynamic.
I think this can be made to work w/r/t non-object-safe trait bounds as well, but I'm not sure how to justify that.
I think library changes which alter which impls get called on which types (i.e. which affect specialization) would still be ABI-breaking, unless you had some more cleverness (type-id-based dispatch for the deoptimized case? ugh) to deal with that complexity.
I also don't think this solution is ideal, but I think it's very important for Rust to be considering these issues, because given the design of the current modern operating system, having separately upgradable libraries is really important. Maybe Rust should be pushing for changes in the OS design, in a direction like NixOS is taking, but it seems like it isn't ready use for building the future's userspace when it can't provide both safety benefit across library boundaries and separate upgradeability.
Yeah, type safety is definitely a big benefit when joining pieces together, but writing normal Rust code does this too, it's only the cross-language boundary that suffers (this is of course the most critical one, so having tools that avoid mistakes here is important, like autogenerating a C header). Joining random codebases together is especially common with cargo making it so easy to pull in dependencies.
Please don't misinterpret me: I definitely don't disagree with you that having in-language type safety and generics are important, but those are fundamentally not things you can guarantee when you're doing things across languages.
> Within a library, it's not uncommon that the core data structure or processing is only correct because a human looked hard at it--I would be surprised if Rust implementations of, say, font rendering, used types internally to guarantee the correctness of their rendering relative to a formal specification. Instead they're using types to make sure they process all input, or make sure they handle memory safely.
I would hope/expect libraries to use types internally. That's the easiest way to have the computer stop you screwing up. E.g. the standard library does, the Vec type contains a RawVec to manage memory ( https://github.com/rust-lang/rust/blob/a212264011289885bdb8b... ) and similarly with HashMap ( https://github.com/rust-lang/rust/blob/a212264011289885bdb8b... ).
> an assumption-free one compiled considering generics to be opaque blobs and using dynamic dispatch for trait methods so that the same code can be used for all instantiations of the generic function.
> For example, a function parameterized over an unconstrained type T can't do anything useful with values of that type, except dropping them. Thus code would be generated in which the call to the drop impl would be dynamic.
This sounds nice on the face of it... but it also sounds outlandishly complicated to implement. On the latter quote, a function can also move data around, and moving (unknown-size) data around seems difficult, e.g. what's an always-works impl of the following?
fn push<T>(x: &mut Vec<T>, y: T) { let z = y; x.push(z) }
Note, no bounds.Calling object safe methods (like a destructor) is the easiest part of implementing this.
> I think this can be made to work w/r/t non-object-safe trait bounds as well, but I'm not sure how to justify that.
I'd be... surprised; non-object safe traits suffer from all the problems of the `push` example, and more.
It's the same reason soldiers only wear heavy armour in the centre of their chests and backs. Why don't we armour the rest? Because the cost outweighs the benefits ... and the tooling (i.e. medevac and modern medicine) makes up for the deficits.
Can you elaborate, in detail, what the costs of memory safety are?
The cost of realizing half-way through adoption that there are things that you need to do, used to be able to do, but cannot do in the new language.
True. In most cases, this is completely outweighed by the benefit of not having these kinds of security vulnerabilities, as well as the other benefits that come with safety.
> The cost of realizing half-way through adoption that there are things that you need to do, used to be able to do, but cannot do in the new language.
Can you explain exactly you cannot do with Rust?
That doesn't match my experience. Source?
> Shortcomings of C are offset by many eyeballs law and the security life-cycle processes so much so that we can afford to safely abandon most organized auditing as well as any defensive coding.
In effect, you're saying we shouldn't care about security vulnerabilities, because they'll be patched. That may be true for your projects. It's not true for many organizations.
No source. There's always a price to pay. Rust is zero-cost, but only on a subset of algorithms that are valid in idiomatic Rust. If you translate Rust to C you won't gain on speed, but if you translate C to Rust you might have to reorganize it and that will cost you. If your subset involved mostly increments and decrements of data and address registers then language called brainfuck would be zero-cost. Correct me if I'm wrong, but I'm led to believe at least some problems that would be most naturally expressed with graph structures will require reference counting when rewritten in safe Rust. Then again there are trade-offs in Rust that I might not be aware of, Singularity exceeded raw C in some instances by replacing overhead and functionality of MMU with static checking in compiler.
> In effect, you're saying we shouldn't care about security vulnerabilities
Cue a rant from spengler/deraadt. I was being sarcastic. We should rewrite most of our software in Rust 20 years ago. ;)
Mostly this is the case, but we do give better aliasing info than C in many cases right now, and that can help optimizations. It depends a lot on your use case.
> Correct me if I'm wrong, but I'm led to believe at least some problems that would be most naturally expressed with graph structures will require reference counting when rewritten in safe Rust.
Most people use petgraph nowadays for graphs, which doesn't use RC; instead it uses indices, which are roughly the same cost as individual heap allocations. They have more computation for lookups, but the computation is usually folded into the addressing mode, and you get better cache behavior.
or use an arena (or backing store of handles, or graph of indices, or ...); which is basically the same situation as doing it in C unsafely with more or less the same cost.
> but only on a subset of algorithms that are valid in idiomatic Rust
IME almost all algorithms can be implemented in safe Rust, just that it takes some work to make it safe (e.g. using an arena, or shuffling some code around). The "limitations" of the borrow checker, for example (where it might be considered overzealous) can always be worked around by twiddling a few lines of code.
If it really comes to that (it shouldn't), you can always make your algorithm work with a few lines of unsafe Rust, and verify that locally. Still better than the entire thing being unsafe.
Regarding the 50% more effort to develop, we've gotten consistent feedback that the effort involved in writing Rust code is not more than any other typed language once you internalize some of its rules (which takes some time, but not too much).
This is only true if you are superstitious or religious and believe that good things that happens to you (or humanity in general) must be balanced out by something bad happening.
The law of conservation of energy, as close to the metal as it gets. I remember reading about the same thing in terms of algorithmic complexity (in the context of data compression), and how one can only shift entropy about. Anybody got any keywords handy for practical examples? My hour of searching ended with conservation of information and black holes...
JabavuAdams is falling for that. Although his inability to control his own violent emotional outbursts is probably going to be the more significant problem in his life.
This is a very empirical view, not a superstition. If you want to learn more about this, and aren't just trolling look at the development of the A4 Skyhawk, the lunar lander, the space shuttle ... hell, any well documented large system.
I wish I could reach through the internet and smack you in the head for such a stupid comment.
I gave you a general reply upthread, but this comment deserves a specific one. You simply can't attack people like this here. I don't think you intended it in a mean way, but it's still the kind of thing we ban accounts for.
ALL of these buffer overflows in C happen because the C stack grows backward, and old space is after new space.
If anyone have enough of a crap just to standardize a calling convention that went the other way, stack buffer overwrites would ALWAYS go into unused memory. Then security-minded people would switch to this calling convention for secure programs, and many problems would be solved.
(Of course heap problems would still exist but they are much harder to exploit and it is easy to make an allocator that tries to confound heap attacks.)
This isn't true, as pointed out elsewhere in the thread. In fact, it even looks likely that this specific bug is still exploitable under your proposal.
Here is the bug: https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb26...
The buffer overflow occurs here: https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb26...
Notice that the buffer overflow occurs not in __libc_res_nquery itself but in one of the functions it calls, send_dg. From a quick glance it looks like the call chain is __libc_res_nquery -> __libc_res_nsend -> send_dg.
Because the buffer is alloca'd in __libc_res_nquery but overwritten in a later call frame, an attacker should be able to overwrite either the return address of send_dg or __libc_res_nsend even if stacks grow the other way. So your proposed fix doesn't address this issue.
edit: typo
char too_small_buffer[256];
function_that_expects_a_big_buffer(too_small_buffer);
There's even a paragraph on Wikipedia debunking this idea: https://en.wikipedia.org/wiki/Stack_buffer_overflow#Stacks_t...Yes, this does not solve every possible stack overwrite. But look at the number that have actually happened in the field and whether this would have dramatically reduced vulnerability in those actual real-world cases. Most times it would.
Most notably, for overwrites happening within the local stack frame, you completely remove the possibility of overwriting the return address. This is a fundamental difference in the level of vulnerability of that kind of code.
It's called "reducing the attack surface". Well-known idea.
Maybe my post was a bit hyperbolic, but I chalk that up to being so annoyed at this.
void
dumb_func(char *user_input) {
char buf[64];
sprintf(buf, "reformatted: %s", user_input);
}
Overflows in functions whose activation records precede the buffer are --- obviously, because they necessarily involve hand-rolled copy loops --- rarer than those that don't.Additional fun fact: a few early generations of mid-90s stack overflow exploits didn't even target the activation records at all!
I think it's interesting and worthwhile to consider whether stacks growing upwards would have made exploit development meaningfully harder, but I also think the answer to that question is straightforward: no, not really.
Something that I've been wondering -- isn't inventing a new language for game developers fighting yesterday's battle?
I've been blown away looking at what Google is doing with C++ indexing, LLVM integration, etc.
I'm experimenting with model-based programming. I.e. toying with the idea that I'm not allowed to write any C++ code, only declarative models that generate C++ code.
This is part of a larger project to essentially automate myself away / transcend, which may or may not be quackery.
Experimenting with model-based programming or whatever other future programming paradigm is healthy. I think we should do a lot of that, because the way we program hundreds of years from now hopefully doesn't look that much like today. BUT, you have to also be aware that there's a reason why these are future paradigms and not current paradigms, and that people building real programs today need to do something that works today. There is no way we could have built The Witness in any model-based system known today.
P.D. It irks me that Rust slogan is 'zero cost' abstraction. There is definitely a cost. The memory model is more complex than C. One shouldn't try to hide the tradeoffs behind 'marketing' slogans.
I woke up in a mood to murder / burn, but I've dialed it down to being rude on the internet. Perhaps by tonight I'll have reclaimed civility.
You don't wear a seat belt either I assume?
Wearing a seat-belt doesn't prevent me from doing anything I want to do, doesn't add any cost to my car that I care about, etc. etc.
There are certainly times where I choose not to wear a helmet, for greater situational awareness, while others would.
We'll be operating at a higher level and just boiling it down to C/asm anyway.
If you want to make a difference, then work on machine programming. If you're inventing a new programming language, then you're fighting yesterday's battle.
Deleted comment