8 ms·
But if sudo were written in Rust, it could have the same level of complexity and not be vulnerable. Yes, it would still be vulnerable to logic errors, like the
by comex 6y ago
But if sudo were written in Rust, it could have the same level of complexity and not be vulnerable.
Yes, it would still be vulnerable to logic errors, like the last famous sudo bug where you pass -1 as the UID. But it wouldn't be vulnerable to this. (And this isn't the first memory safety bug to be found in sudo.)
Yes, sudo's complexity is useless for 99.99% of its users. But wouldn't it be nice if the result were merely a gross feeling rather than a security hole?
- ddevault 6y ago>But if sudo were written in Rust, it could have the same level of complexity and not be vulnerable. This is not true. Complexity breeds bugs, including security bugs, and memory safety doesn't change that. Your example is a good one - here's another: doas once failed to limit the environment variables which are passed to the child process, which could be used to nefariously influence the program running (e.g. with LD_PRELOAD). How would Rust prevent that oversight? It wouldn't. A simpler program will generally be more secure than a complicated one, no matter what language either is written in. Furthermore, rewriting an established program from one language to another will always introduce more bugs than it fixes, and more severely the more complex the program is. The single best way to improve security is to reduce the attack surface, and the single best way to do that is to reduce the complexity of your system.
- zamadatix 6y agoIf you go a little further with the quote: "Yes, it would still be vulnerable to logic errors... But it wouldn't be vulnerable to this. " I think you'll find in disagreeing with the comment on logic errors you just said the same thing the comment did about logic errors. Also I think the generalization that rewriting an established bit of code in a new language in a secure language is a bit too general. clearly Firefox not only set out to make Rust for this purpose but it's not had an explosion in vulnerabilities with the modules it has replaced. Quite the opposite actually. Nor has every tool or app rewritten become a security failure compared to the original. I do think it's something that can easily be screwed up though, especially if someone rushes through by focusing on functionality duplication instead of building a more secure version of something. Regardless, both "using a memory safe language results in a more safe program" and "having a minimum attack sufrace results in a more safe program" can be true. There is no need to make it a choice of A or B.
- ddevault 6y ago>I think you'll find in disagreeing with the comment on logic errors you just said the same thing the comment did about logic errors. I think you'll find that my comment explicitly acknowledges this and expands on it with another example. Are we done telling each other to read the things we're writing? >Firefox not only set out to make Rust for this purpose but it's not had an explosion in vulnerabilities with the modules it has replaced. You're setting the bar pretty high with an "explosion" of vulnerabilities here. Rust programs have vulnerabilities, including rewrites. They also have other kinds of bugs, often ones which were not present in the code that they're replacing. You need only browse your nearest convenient RiiR bug tracker to find evidence of this. Let me restate my thesis in mathematical terms. If we presume that 1 in 100 lines of production code has a bug in it, regardless of language (generous, I know), and that 1 in 10 bugs in C programs are memory corruption related, then saving 10% of those bugs by rewriting it in Rust would take a 10,000 line codebase from 100 bugs to 90 bugs. A 1,000 line codebase, still written in C and without the advantage of memory safety, would have only 10. In today's example, sudo is a caricature of runaway complexity. Rust is often touted as a panacea, but C has very little to do with why sudo is insecure. Sudo is comically overengineered and that level of overengineering has no place in a security context. This is the larger issue that needs to be addressed, not Rust.
- vitno 6y agoI agree Rust is not a panacea and that rewrites create their own set of problems, the only issue with this analysis is assuming 1/10 bugs are memory corruption related. Both Chrome & Microsoft found about 70% of bugs to be memory safety related. I've heard similar numbers out of FB as well. The math looks a little different with that data. https://www.chromium.org/Home/chromium-security/memory-safety https://www.chromium.org/Home/chromium-security/memory-safet... https://www.zdnet.com/article/microsoft-70-percent-of-all-security-bugs-are-memory-safety-issues/ https://www.zdnet.com/article/microsoft-70-percent-of-all-se...
- ddevault 6y agoEven if we run the same math with 7 out of 10 bugs being memory safety related, and assuming that Rust prevents all of them, those same example programs end up with 30 bugs in Rust and 10 bugs in C. There's another argument I could make, too. Look at the bug tracker for the program you want to rewrite in Rust, examining the historical bugs. You'll find that there are often hundreds or thousands of mistakes that they made and already fixed in the original codebase. If you're rewriting it from scratch, can you be sure you won't make just as many? A stable, maintained codebase with a low throughput of changes tends to have fewer bugs over time, as the lack of churn avoids introducing new bugs and the application of time susses out all of the existing bugs. Rewriting the whole thing from scratch has a very high rate of churn, introducing a whole new slew of bugs on its own. Now, a small codebase, focused on delivering its key value-adds without distractions, kept stable and at a low-churn rate over a long period of time: no matter what language you use, this is the best recipe for reliability and security.
- comex 6y agoIncidentally, after inspecting doas for a few minutes, I found two near-vulnerability bugs in it. The first bug lets any user cause doas to read out of bounds of an array, though not in a way that's exploitable. Well, it's arguably a bug in libc. If you run doas with a completely empty argv (argc = 0, so not even an executable name; the two systems I tried, Linux and macOS, both let you do this), getopt will exit with optind = 1. Then when doas does; argv += optind; argc -= optind; `argc` will become negative, and `argv` will advance past the null terminator. On most OSes, the `argv` array is immediately followed in memory by `environ`, so argv will now point to the list of environment variables. doas will then dereference argv, and generally act as if you tried to execute a command consisting of the environment variables. However, the environment variables are not secret, and doas doesn't behave any differently than if you just passed the environment variables as normal command-line arguments, so this is not exploitable. On an OS where argv is not followed by environ or a similar array of character pointers, doas might crash instead, although since it only reads from those pointers rather than writing to them, this still probably wouldn't be exploitable. The second bug would compromise memory safety if things were slightly different. The bug is in configuration file parsing. Even if it did compromise memory safety, it would not actually be exploitable, because doas normally only parses the trusted systemwide configuration file. It can be asked to parse a configuration file passed on the command line, but it drops privileges before doing so. This is a good example of layered defense, so kudos to doas for that! Still, I thought the bug was worth mentioning. The bug is a traditional sort of integer overflow. parse.y grows the array of rules with maxrules *= 2; but maxrules is an int, so this will eventually overflow if the configuration file is large enough. However, because maxrules happens to be signed, before doubling produces a smaller-than-expected positive value, it will first produce a negative value. This will then get sign-extended when converting to size_t (assuming 32-bit int and 64-bit size_t), and reallocarray's overflow check will trigger, causing reallocarray to return NULL. doas interprets that as out-of-memory and handles it cleanly. (On a system where sizeof(int) == sizeof(size_t), things are a bit different, but it will just run out of memory before maxrules gets that high.) Moral of the story? Well, as I see it: Simplicity and layered defense, both featured in doas, are both effective ways to avoid vulnerabilities. But guaranteed memory safety, which would require a different implementation language, is also an effective way to avoid vulnerabilities. You aren't forced to pick and choose. Why not demand all three?
- staticassertion 6y agoA lot of your statements are pretty strong, and imo totally incorrect. > Complexity breeds bugs, including security bugs, and memory safety doesn't change that. Yes, memory safety changes that radically. > A simpler program will generally be more secure than a complicated one, no matter what language either is written in. Disagree, but the statement is really weak anyways, especially since 'complexity' is an ill-defined term. More features? Cyclomatic? > urthermore, rewriting an established program from one language to another will always introduce more bugs than it fixes, and more severely the more complex the program is. Should be obvious to anyone that this isn't true. > The single best way to improve security is to reduce the attack surface, Not true, but it's a great way to start.
- ddevault 6y ago>Disagree, but the statement is really weak anyways, especially since 'complexity' is an ill-defined term. More features? Cyclomatic? I'm not sure of any definition of complexity you could appeal to which makes my argument weak. >>rewriting an established program from one language to another will always introduce more bugs than it fixes, and more severely the more complex the program is. >Should be obvious to anyone that this isn't true. The opposite is painfully obvious: (1) Writing code causes bugs. (2) Rewriting an established project involves writing more code than leaving it would. (3) Writing all of that new code will introduce new bugs which were not present in the original.
- staticassertion 6y agoYeah I think that's an absurd reduction. Rewriting code means that you can solve fundamental architectural issues, that you can start fresh with better tooling, that you have the lessons learned without the technical debt, etc.
- ddevault 6y agoYeah, but why the assumption that those things are an issue? We're talking about mature codebases. Rewriting it again in C would also give you a chance to start fresh with better tooling, lessons learned, paying back tech debt, etc. Even still, you're going to introduce new bugs in the process. You might fix a few hard-to-address architectural issues, but all of the other bugs would be easier to fix in the original codebase than by rewriting the whole thing. I'm not saying that a rewrite is never justified, but rather that the argument that we should rewrite in Rust simply to avoid bugs has little weight.
- jodrellblank 6y ago> "Furthermore, rewriting an established program from one language to another will always introduce more bugs than it fixes" Here[1] is a link to a slideshow of a talk on the F# language, with a case study from EON PowerGen company rewriting the core of an application evaluating revenue due from their balancing services contracts nationwide in the UK. It was originally 350,000 lines of C# developed by 8 people in 5 years and incomplete. It was redeveloped by 3 people (2 had never used F# before) in 30,000 lines, complete in 1 year. They claim zero bugs in the F# redeveloped system (page 29). This example also gets a mention in a Don Syme (F# language designer) talk in 2018[2] with the PowerGen employee in the audience. The PDF cites a testimonial from Kaggle saying they're moving more and more of their application into F# which is "shorter, easier to read, easier to refactor, and because of strong typing, contains far fewer bugs". [1] https://www.microsoft.com/en-us/research/wp-content/uploads/2014/02/kenji-takeda.pdf https://www.microsoft.com/en-us/research/wp-content/uploads/... [2] https://www.youtube.com/watch?v=kU13g_noAQM https://www.youtube.com/watch?v=kU13g_noAQM
- musicale 6y ago> But if sudo were written in Rust, it could have the same level of complexity and not be vulnerable I'm puzzled that we don't have a memory-safe ABI (e.g. amd64-safe) and runtime for C so we could just compile things with clang -safe sudo.c to avoid memory errors. I'm fine with sudo (or whatever) taking a 60% performance hit to be more reliable - processors are thousands of times faster now than they were in 1980 when sudo was written. If we had a memory-safe ABI for C/C++ in common use its performance overhead could probably be reduced significantly over time due to implementation improvements, and we might see hardware acceleration for it as well. There are a number of proof-of-concept memory-safe compilers for C using fat pointers, etc., but memory safety hasn't made it into gcc or clang. 64-bit CPUs can help because you can repurpose address bits. Even Pascal (which is largely isometric to C) supported a degree of memory safety via array bounds checking. I believe Ada compilers also support memory safety. PL/I was actually memory safe and is why Multics never had any buffer overflows. Obviously Rust is memory safe, but for a lot of legacy C code it is impractical to rewrite everything in Rust but eminently practical to recompile it with memory safety turned on.
- scatters 6y agoIt's called AddressSanitizer. You enable it with the compiler flag -fsanitize=address. It's supported by clang, gcc and lately MSVC.
- rkeene2 6y agoYou shouldn't use ASan in release builds since it may have exploitable vulnerabilities.
- saagarjha 6y agoAddress Sanitizer is not perfect, nor is it suitable to ship in production.
- kevingadd 6y agoAsan binaries should never be shipped in security sensitive environments. It's not designed for that. It's unsafe.
- gameswithgo 6y agoRust would have prevented the -1 as a UUID too, because you would have used a sum type (Rust enums) instead of a sigil there. Its easier, its idiomatic, its more clear, and the compiler knows how to optimize the overhead away a lot of the time.
- comex 6y agoWell, in this particular case, the special behavior of -1 is baked into the setresuid system call, while sudo thought it was just an ordinary UID. So if you look at one of the Rust operating system projects designed from scratch from-scratch OS designs, it might not have this kind of pitfall. But if you literally just reimplement sudo for existing OSes in Rust – which I think would be a neat project for someone to take on – you’d be at risk of running into it.
- j16sdiz 6y agoIt is a uid (as in user id), not uuid. Don’t think you can use sum type for that
- steveklabnik 6y agoIt is a user id, but that bug happened because a -1 was being returned as an error code in one place, and then being accidentally passed in another place. The sum type would be used as the “this possibly errors” return type in the first function, making the bug effectively impossible to happen by accident.
- 0xbadcafebee 6y agoIf sudo were written in Perl, it would also not be vulnerable.
- MayeulC 6y agoI thought about that. But when using a higher-level language (that comes with a runtime), you need to give privileges to the whole runtime, which arguably has a much bigger attack surface, unless I am mistaken? The kernel won't allow you to setuid scripts, there is a reason for this: it's very easy to leave glaring security holes while doing so.
- 0xbadcafebee 6y agoPerl had a fix for this called suidperl, which was a wrapper which enabled Taint mode and other strict checks (https://perldoc.perl.org/perlsec https://perldoc.perl.org/perlsec). I don't know of any other language or interpreter that go this far for security by default, so Perl might be the most secure language in this regard. However, suidperl was dropped in 5.12. My main point was that you could rewrite sudo in all sorts of languages, but saying "just rewrite it in Perl" (assuming it worked) isn't a enough justification to make it happen. Nobody is going to re-create their own project in Perl, Rust, etc just to eliminate buffer overflows. If somebody wants sudo in Rust, they'll have to do it themselves, and it still might never replace the original.
- MayeulC 6y agoThank you for pointing that out :) If someone would get serious about security, auditing every setuid binary would certainly be something on their list (if they use any). If they really want the functionality, rewriting it to cover just enough of the required functionality wouldn't be unheard of.
- 0xbadcafebee 6y agoI agree. But really the security best practice is to remove setuid from all binaries, and rely on RBAC rules (such as with SELinux). That would solve more security issues than anything else, but this is way more effort than most people are willing to invest in security.