Currently vendoring all of the dependencies for Rust implementation pulls in 1.75 million SLOC, which I find amusing.
This is a lot of SLOC, and a huge surface area to pull targeted supply-chain attacks, IMO.
P.S.: I know you don't compile in all of this into the binary, yet consider the eyes and work hours required to verify that the whole chain is sane and safe.
The stdlib only exposes some syscalls via rust-friendly wrappers, and in a way that is generally a cross-platform subset, sometimes with platform extentions. So the `fs` module (https://doc.rust-lang.org/std/fs/) exposes common file operations across most (all?) supported platforms. Some unix specific file operations are exposed at: https://doc.rust-lang.org/stable/std/os/unix/fs/index.html . These things do a pretty good job for most work, but sometimes you gotta get wierd...
The libc crate allows more direct access to the syscalls on your flavor of *nix by creating a rust-> bridge for them, and exposing the C types directly. This bridge isn't a lot of code, mostly it just does the work of creating a rust function that minimally wraps the external C function. For a lot of low-level software you end up pulling the libc crate to hit your system specific calls.
And for some very system specific calls (e.g. io_uring) you end up having to pull in another crate that calls into that subsystem for you (often pulling in libc also).
All of this ends up being linked to the libc you build against tho.
It does matter, because to get a comparable number from sudo you'd have to count libc. I don't know how many lines of code that is exactly (and e.g. musl or OpenBSD libc is probably smaller than GNU, so there isn't a fixed number to start with), but I bet it's roughly a comparable number, or at least much closer.
As a general point I agree with you, but both sudo and sudo-rs are pretty large.
libc is used by the entire system. It's not a vendored dependency of just sudo. At best, you'd amortize it over everything that uses it to get a reasonable comparison.
Are we not on the trail to doing the same with rust, at least in theory? You don't think someone somewhere has the idea to make an all-rust system eventually? Even without that evetual ultimate expression, if you have even a few rust components in your system and not just sudo alone, then the same point stands. You don't count rust itself any more than you count gcc.
The sudo-rs Cargo.toml [1] file seems very reasonable. This is the curse of being cross platform. The inclusion of https://github.com/Stebalien/tempfile as a dependency is responsible for the overwhelming majority of lines due to including *-sys crates for multiple OSs.
In general, you have a very valid point, but how many lines of code do we need to build the normal sudo if you are generously adding stuff we don't compile into the binary? Compiler, tools, some machine with some userspace and kernel for those to run on and so on?
Arguing that complexity comes from SLOC feels like paying per LOC... it sort of misses the point.
The languages are different - a lot of C behavior feels "inferred" or "implicit". A lot of Rust behavior is explicit, that is you have to write down exactly what's happening. So things like casting a void* to a $whatever require a couple of lines of rust, not just a single line (or fragment) of `($whatever *) p`.
My personal experience is that the explicit nature of Rust is pretty nice when visiting new code, or revisting code I wrote a while back - everything is written down for me, whereas I have to puzzle out a lot of behavior from the C. It's a bit annoying at first, "cmon compliler, why do I have to tell you this?" is still a common refrain in my head, however its worth it in the long run - revisits to the code are much faster to grok/reload, and once I got used to it, writing it down as it was all loaded in my head the first time wasn't so much of a pain anymore.
I’m on the sudo-rs team: we are actually very mindful of the dependencies we use. Our current main branch already uses significantly fewer dependencies than a few months ago. Aside from security it also really helps with adoption since it makes packaging way easier. A large part of the output you got is due to a tempfile dependency, which is a dev only dependency that is not touched when compiling a release/dev binary, only during testing a small part of this would be used. I say a small part because most of it is related to tempfile running on windows, which is irrelevant for sudo-rs since we don’t support or intend to support windows.
Just for good measure though and to prevent any further discussion about this I’ve removed the tempfile dependency from the main sudo crate as our usage of it could easily be replaced by a simple timestamp/pid combination. But again, this really only affected testing, I think the size of the code that ends up in the final binary is very reasonable.
As for people suggesting supply chain attacks via dev dependencies: I doubt we would be the final target of such an attack: i.e. what an attacker would really want is access on all/some machines that have sudo-rs installed. The only way to do that would be to change the release artifacts, which dev dependencies do not have the ability to change, at least not directly, so such an attack would only be the first step in a chain of attacks. I have a feeling that there are way easier and less detectable ways of manipulating us than by using modified dev dependencies. Of course that doesn’t mean we should ignore the risks.
`cargo vendor` will download dependencies and dev dependencies for all platforms, which leads to a lot of unused code being pulled in. In this case, the Windows API and Microsoft compiler wrappers.
In this instance, during the build process "tempfile" is used as a dev-dependency, which has a runtime dependency on windows-sys when compiling Windows binaries. I'm not entirely sure why (commenting it out in Cargo.toml doesn't seem to break the build).
After commenting it out and manually removing the spurious Windows API files as well as the unrelated packages (`cd vendor; rm -rf ctor diff output_vt100 pretty_assertions proc-macro2 quote syn unicode-ident yansi win*`), I get the following results:
Thanks for this summary. I was going to write something about the safe rust sudo depending on a load of handwritten assembly but it seems that's a spurious windows thing.
The dependency list[0] looks pretty reasonable, AFAICT the overwhelming majority of that line-of-code count comes from autogenerated Windows API methods.
If you're intending to install the package on an image, [dev-dependencies] are not going to be included in the package. So, no, it's not actually relevant to the surface area of the package.
If a C library used Python to run its tests, I don't think we would consider the whole Python interpreter to be part of the software supply chain for that library. Sure it's possible that running tests on a build machine could let an attacker corrupt the build later, with a bad PyPI package or something. But that feels more like a "not having a clean build environment" problem than a "this project has too many dependencies" problem. I think the fact that Cargo manages these two lists in the same file makes the relationship feel tighter, but I'm not sure it's actually tighter.
I see your point, but if you're going to consider all code that runs on the dev machine as a source of supply chain attacks, that's going to include all LOC for the Linux kernel. And the LOC for dev's web browser that they use to browse the issue tracker. And so on.
It's weird. You'd think Rust has been around long enough that they could finally take the step toward a mature implementation with a stable ABI and support for shared libraries.
It's OK as long as you're just prototyping the language and need to make changes fast. But I'd hope that we could one day actually build a real system in Rust.
As others have mentioned, there is a long list of sudo CVEs that are unrelated to memory safety. I didn't see it mentioned in the article, but I hope that they have mined the CVEs for tests to ensure they don't accidentally reintroduce a known vulnerability. I was also surprised to see no mention of tests originally written for ogsudo, surely there are some?
Overall, I appreciate the rewrite-it-in-rust 'movement', I think it is an excellent learning opportunity for people who may not otherwise bother with learning the details of the foundations of our modern systems. And, as in this case, taking a detailed look at the original can improve the original.
Do you know if sudo uses negative tests for their CVE fixes?
If they are written in such a way that they are portable (i.e. execute sudo, send mangled data, inspect response) it shouldn't be too hard to run it against the new version.
At least that is what I try to practice in fixing all kinds of bugs. Write test that proves the bug, fix the bug, write test that proves bugfix works, invert the test that proves the bug.
One of the famous non memory safety bugs in sudo recently was related to using a sigil, which Rust would have prevented because the natural and easy solution in Rust for those use cases is an Enum/Sum Type.
Which is to say that Rust has safety features beyond just what we are used to from garbage collected languages. Sum types, stricter typing, data race protections, etc.
And yet, Rust is being taken seriously in many places where those languages have been unable to displace C. I wonder if it hadn't taken 15 years for a FOSS Ada compiler to become available, maybe it would have taken off.
I suppose my point was that Ada had missed it's shot-by the time GNAT came out, the buzz of an exciting new language expired before people could easily use it.
I'm not familiar with the marketing that Mozilla did, but whatever they did it does seem to have been effective. However, I do think that the Ada mandate by the DoD would be equivalent or more effective in kick-starting an ecosystem.
DoD software and the commercial / FOSS world rarely intersect. The last time the government tried to set a computing language standard, we got COBOL, so it's probably for the better such a thing isn't tried again.
The marketing Mozilla employees have came up with is creating a Categorical Imperative for using Rust. The person in the comments section of a C project or any CVE bug will be saying, this would never have happened if this was written in Rust, why aren't you writing this in Rust? The moral thing to do was to write it in Rust! And a lot of software engineers are eager to think and talk like this, because it gives them something more exciting in their lives beyond writing bean counters for selling widgets. Pure functional languages already had a bit of that attitude, but I don't think it caught on nearly so much because actually writing pure functional code is very difficult even for most engineers.
Is there evidence that this was a concerted marketing effort? That behavior seems pretty consistent with the normal culture of the sector, vim vs emacs comes to mind.
I have found rust difficult to learn, and I continue to develop and maintain embedded systems with C. I say this not to distance myself from rust, but to indicate my neutrality on the subject.
> The last time the government tried to set a computing language standard, we got COBOL,
Ada, actually (well, that might not be the last time, but it is certainly much more recent than COBOL.) COBOL and JOVIAL were roughly concurrent – both efforts started in 1959, I believe – but the effort that culminated in Ada started in the mid-1970s.
I should have clarified that I meant a universal standard. COBOL was meant to create a common language across business and government, while Ada was meant to just consolidate a language across DoD projects. Ada is a much better language than COBOL, but I think that adoption of even a good language across those ecosystems will only succeed when it is adopted from the ground up.
COBOL also wasn’t a government-launched initiative, it was an industry initiative that went to the government for funding, unlike Ada which was born as a DoD initiative.
If COBOL was worse for its time than Ada was for its time (which I don’t think is clear, though I’d rather work with Ada than COBOL if I had to pick one today), maybe that would support an argument about customer-need driven projects vs. vendor-imagination-driven projects.
We did indeed look at and are still looking at the list of CVEs where sudo takes part right now. However, many of the bugs are through one way or another not applicable for our implementation (yet). For example CVE-2014-0106 is only applicable when env_reset is disabled, but we purposely do not support that mode. Or take for example CVE-2023-28487 which concerns sudoreplay, but right now we have no plans to add that functionality.
Sudo does have a suite of regression tests, but they mostly test specific implementation details which makes it hard to port to our codebase. Our test suite tests against the integrated artifact, which allows the easy switching between the two implementations (and also allowed us to find a few bugs in the original sudo).
If you want to move away from Sudo, but don't want to try this rust implementation just yet, I have had great success with OpenBSD's doas. It has been ported to every Linux distro I know of as well:
doas is great with 2 caveats (from my experience on debian): the persist feature has a sketchy implementation that didn't work; and plenty of other software assumes you use sudo and may require it as a dependency.
Regarding software support - the situation is definitely improving. I remember being pleasantly surprised that various Arch package management tools support it out of the box and I didn't even bother installing sudo on Debian.
On a basic level: programs written in C almost always have memory access patterns that can't be proven safe generally. sudo, in particular, has had a few public bugs over the past few years that directly trigger potentially exploitable memory unsafety[1][2]. Note that not all bugs receive public reports, much less are assigned CVEs.
Rust's memory semantics are safe by construction: unless you intentionally write the the part of the language that requires you to explicitly mark things as unsafe, your programs cannot contain the kinds of temporal or spatial memory bugs that can occur in C and C++. Given that, calling this the "memory safe" implementation seems pretty reasonable, in the same way that calling a Java or Python implementation of sudo "memory safe" would also be reasonable.
That each of the versions dannymi listed had at least one potentially exploitable issue that a natively memory safe language would have mitigated, versions actively used in production as recently as this year even by people who keep bang up-to-date?
I'm sceptical that the chance of introducing new and interesting bugs of other varieties with a rewrite is worth the protection offered by the new environment¹, but either your dismissal of dannymi's point is rather disingenuous or you genuinely were missing something that had been stated quite obviously…
--
[1] except where existing problems are so systemic that fixing them in the original stack would effectively be a partial rewrite anyway, so susceptible to the same risk
> That each of the versions dannymi listed had at least one potentially exploitable
Can you please point me to each one of those?
I can only see a few of them.
> memory safe language would have mitigated
potentially, assuming there are no bugs anywhere in the toolchain.
> but either your dismissal of dannymi's point
I wasn't dismissing no one's point.
I simply scrolled through the list of issues and found out that the majority of them were things like (picked them randomly)
systemd before 247 does not adequately block local privilege escalation for some Sudo configurations, e.g., plausible sudoers files in which the "systemctl status" command may be executed. Specifically, systemd does not set LESSSECURE to 1, and thus other programs may be launched from the less program. This presents a substantial security risk when running systemctl from Sudo, because less executes as root when the terminal size is too small to show the complete systemctl output.
An issue was discovered in Zimbra Collaboration (ZCS) 8.8.x and 9.x (e.g., 8.8.15). The Sudo configuration permits the zimbra user to execute the NGINX binary as root with arbitrary parameters. As part of its intended functionality, NGINX can load a user-defined configuration file, which includes plugins in the form of .so files, which also execute as root.
A privilege escalation vulnerability in FortiNAC version below 8.8.2 may allow an admin user to escalate the privileges to root by abusing the sudo privileges.
It was found that cifs-utils' mount.cifs was invoking a shell when requesting the Samba password, which could be used to inject arbitrary commands. An attacker able to invoke mount.cifs with special permission, such as via sudo rules, could use this flaw to escalate their privileges
I am genuinely asking if memory safe languages could prevent this kind of issues, which represent the overwhelming majority of the issues reported on that specific page, and how.
I've not read into them in detail, but they are all overflow issues that other languages could have mitigated. Of course they may mitigate them by the task falling over at run-time, but that is “failing safer” than continuing with (potentially deliberately) corrupted data.
---------------------------------
I only spotted the extra details you added when I submitted my first reply (had the reply form open for a while due to work distractions):
> I am genuinely asking if memory safe languages could prevent this kind of issues, which represent the overwhelming majority of the issues reported on that specific page
That page is listing all issues in sudo logged in that database, from which the relevant data was summarised (basically the OP was citing his source). No claim was being made that increased built-in memory safety would prevent all the issues (or even many of them) in that list.
interestingly these would not be a problem in a language like Pascal that is not memory safe, but has runtime bounds checks enabled by default or C++ that has flags to enable it
So we are left with one over at least a 100
another interesting thing IMO is that the bugs have been sitting there for a long time, so they are probably not obvious and nobody can be sure there aren't similar sleeping bugs in today's software written in safer languages.
This is not against memory-safe languages, mind you, I prefer Rust over C or C++ any day, I simply found that the original claim seemed too bold compared to the actual data and failed IMO to prove that sudo is ridden by easily exploitable memory bugs.
All the versions/ranges listed are associated with relevant CVEs on the list, which of the original posters claims can you not see backed up by that list?
> Of the listed CVEs, not many of them are due to memory safety.
That is a list of all the CVEs for sudo, no claim was made of implied that they were mostly due to issues that memory safety would help with, the poster gave the link as a source for information that was presented.
> no claim was made of implied that they were mostly due to issues that memory safety would help with
he literally did
this is the entire message
Sudo 1.8.0 to 1.9.12 (the latter is from 2023(!)) are *memory unsafe*.
Sudo before 1.9.5p2 is *memory unsafe*.
Sudo before 1.8.26 is *memory unsafe*.
Sudo before 1.6.6 is *memory unsafe.*
memory unsafe here is used for dramatic purpose by the author
it actually means that
Sudo 1.8.0 through 1.9.12, with the crypt() password backend, contains a plugins/sudoers/auth/passwd.c *array-out-of-bounds error* that can result in a heap-based buffer over-read. This can be triggered by arbitrary local users with access to Sudo *by entering a password of seven characters or fewer. The impact could vary depending on the system libraries, compiler, and processor architecture.*
in this light, is PHP memory safe because it enforces runtime bounds checks on arrays?
Or
In Sudo before 1.8.26, if pwfeedback is enabled in /etc/sudoers, users can trigger a stack-based buffer overflow in the privileged sudo process. (pwfeedback is a default setting in Linux Mint and elementary OS; however, **it is NOT the default for upstream and many other packages**, and would exist only if enabled by an administrator.)
He literarily, both in the traditional (literally) and modern (figuratively) meanings, did not.
Stating that the page is the source for the information summarised does not mean the same as claiming all/most of the information in the page specifically refers to those cases.
A number of those entries reference multiple overflows.
And a memory safe language will protect against some overflows on the stack (overflows within frames corrupting other values, rather than errant loops that blow the stack by creating too many frames and/or too large frames).
Are you aware of what an exploitable scenario of using too much stack space looks like?
The buffer needs to be so large that it not only exceeds the offset to the guard page, but it reaches a non-faulting address. Lastly it needs to be accessed from the front first rather than the back.
I don't know if compilers commonly generate benign memory accesses from the back of the buffer for large stack allocations to get the page fault handler going. I thought that they did after some prominent Linux exploits in this area. If they do do that, this is safe. Also, this issue would also affect the rust compiler, so they must employ that strategy if this works.
How many hundreds of thousands of lines of code do you think sudo is?
How many hundreds of thousands of lines of churn do you think happen per year in sudo?
If your answer is: What do you mean hundreds of thousands? That is the right question, but the wrong answer. The answers being around ~500,000 and on average ~200,000, including this year, respectively.
In contrast, OpenBSD doas, which exists to serve the same primary purpose of executing commands as a super user, clocks in somewhere around a few hundred to maybe 1 or 2 thousand lines total just eyeballing it.
> If your answer is: What do you mean hundreds of thousands? That is the right question, but the wrong answer. The answers being around ~500,000 and on average ~200,000, including this year, respectively.
> In contrast, OpenBSD doas, which exists to serve the same primary purpose of executing commands as a super user, clocks in somewhere around a few hundred to maybe 1 or 2 thousand lines total just eyeballing it.
It seems to me, with those numbers, that the big problem with sudo is not the language it is written in but the extremely large attack surface.
I would guess that for 1 out of every million invocations of sudo, all that extra functionality is needed. For the rest of the time, its merely being used to execute a single command as root.
We could make systems more secure by simply removing sudo from those systems that don't use that extra functionality, and replace it with doas.
Most software that handles actual human lives on the line is not written in plain C. Writing 'security critical' software in C is a trade off - many people are willing to accept extra defects in return for the benefits of the C ABI and performance.
To be blunt, I don't think that this really matters very much. The reason is that if you used Pascal or Modula-2 , both of which use "counted strings" you would very likely have the same kind of safety.
I think the potential of Rust is greatly oversold, because it only ever gets compared to known-unsafe cases, such as string handling in C; there are plenty of other languages that handle this safely out of the box.
And Modula-2 is in the GCC 13 compiler and FreePascal has been out for over a decade at this point; both languages are far more compact and have formal specifications...
> I think the potential of Rust is greatly oversold, because it only ever gets compared to known-unsafe cases, such as string handling in C; there are plenty of other languages that handle this safely out of the box.
The comparison is apt because of scope and scale: Rust is intended to integrate into and eventually replace C codebases; both Pascal and Modula-2 can integrate with C, but AFAIK do not place substantial emphasis on doing so (nor have substantial amounts of community interest in doing so).
Similarly: people keep doing string handling in C, so that is what is going to continue to matter. They also keep doing other things in C that Rust prevents, which are also going to continue to matter. The fact that they're known to be unsafe doesn't change that.
Put another way: it's unclear that the interest in Rust (which is immense) would ever translate into equivalent interest in Modula-2, any technical merits aside. The reality is that Rust does these things well, and a large number of people like it and are successfully using it to replace or augment C codebases. Both conditions are necessary.
The framing in the last two lines only makes sense if you consider the amount of total bugs as a prior. In practice, the more realistic prior is something that's more observable, e.g. the number or rate of published CVEs or of security breaches. If something like that can be reduced by 70%, that's much more significant than your framing of "0.7% of all bugs in this example are security bugs due to memory safety" makes it out to be.
Bugs mean the software's behaviour is outside what was specified, but for security we're allowed to depend on any aspect of the specified behaviour. So, all bugs are security bugs.
Some bugs aren't as important as others, but they're all just bugs.
For everything else? I think you have not worked on much software touched by the end-user.
A few years ago, on a security-critical product (chipcard/swipecard terminals)[1] written in C++ I specifically did an analysis on tickets logged as a bug over a 3 year period.
It's a fairly mature product with the original devs who built it still working on it.
Very large surface area - 100s of thousands of deployments at merchants, sales ringing up at all those merchants almost constantly, with a variety of different inputs that the terminal had to handle[2]. The environment can be so random in its input at times it's halfway to fuzzing-in-the-field.
IIRC (I am no longer there), there were maybe a few hundred tickets out of a dozen thousand or so that were actual bugs.
Of those bugs exactly three were memory safety issues.
So, your statement:
> So, all bugs are security bugs.
appears to me to be entirely imaginary and fantastical. To me, who has been developing professionally since the min-90s, that statement appears to be purely fiction. I've not worked on any system where even close to 10% of the bugs turned out to be security issues.
"Missing postcode field in address input" is not a security issue. "Retry not attempted when host is down" is not a security issue. "Incorrectly rejects valid ID number" is not a security issue.
The clear majority of bugs are not security issues, even in a product which is intended to be a security product.
[1] I was an EMV developer.
[2] In case you think this is a simple thing, think Foreign Currency purchase, foreign issuer, Fallback from Tap->Chip->Swipe due to corrupt cards or floor/merchant/bank/cardholder/issuer limits, cancellations of transactions that may/may not have completed due to network errors, power cycles in the middle of a transaction, card switch-out attempts, PoS errors, EMI causing random data corruption in serial transmission lines (those chillers next to the till that hold Red Bull and things have compressors come on and go off intermittently, causing spikes).
Then there's also the fact that the terminal sold cellphone airtime for various SPs, PAYG electric vouchers, scanned QR codes, fingerprints. They also, when deployed to forecourts, had code to handle fuel prices and quantities automatically, split out receipts for oil separate from petrol and accept rewards cards linked to retailers.
When deployed at a restaurant, it had code to allow atomic multi-party payments, even when each party was on a different switch, to a different bank, to a different issuer.
It had code to talk to the most popular PoS systems (each one using a different application layer protocol even if they were all using BER/DER at the base layer).
That's not counting the health monitoring code, or the OTA updates code, or the TLS code (yes, it had its own because certifying OpenSSL or similar was just as expensive as rolling your own with the bare minimum necessary, and certifying that), or code specific to each bank, or key injection code, or cryptography code.
It's anything but small or simple. The average YC startup almost certainly has nowhere near the same amount of complexity, nor does the code you are used to working with have anywhere near the same amount of criticality or attempts to exploit.
In other words, this is a fucking complicated source codebase, with constant attackers who aren't interested in DDoSing you, and severe financial repercussions per second when it is down, while profiting successful exploits immensely.
I almost couldn't ask for a better example. EMV is so broken that arguably even if systems did behave as specified that's a security bug anyway, because it doesn't fulfil people's basic expectations of how such a system should work.
For people tuning in at home, EMV stands for Europay, Mastercard, Visa, it's the standard for modern "chip and PIN" type payment cards, including "tap to pay" designs and these days is controlled by a consortium or something which allows all the various payment card outfits to dick each other over equally rather than just those three.
Anyway, you seem to just assert that there weren't security bugs, but with no evidence whatsoever for your claim. I have no doubt that you confidently believed that things which actually were serious security bugs in your EMV terminal were fine. After all, why would this standard be ludicrously fragile and have key security requirements that you might destroy by mistake ? Only something designed by idiots would do that...
I think the main thrust of your view is that you deemed the tickets in your ticket system (which may have been labelled "bugs") not to really be bugs at all. That's a very different semantic argument, and mainly I'd say you should take it up with management. At my current employer we distinguish requests, incidents, cards, tickets, bugs and work items, and it feels like that's maybe too much, but whatever.
In security software like sudo you could argue that nearly all bugs are security bugs. But there are plenty of bugs in non-primarily-security software where it's really hard to argue that there is any security implication.
In any case the reason for the figure is because when there is a buffer overflow that allows overwriting the stack its almost always a security bug because it usually allows untrusted input to make the program do anything. So of all the kinds of bugs a program can have the memory safety ones are usually security bugs, while other logic bugs may or may not have meaningful security implications.
In a suid program that is designed to launch other programs (e.g. sudo), significant logic bugs are very likely to have security implications too.
This is supported by sudo's CVE list-- the vast majority appear to be logic bugs with no memory safety involvement. One argument made for rust is that with memory safety "in the bag" developer time can be better invested in other areas. But it isn't safe to assume that: Rust is a very different language and other properties of it such as its far greater complexity than C may make logic bugs more likely. As far as I know the defect rate in rust software has never been seriously studied.
It would be a huge relief if there were some serious analysis that could provide some confidence that rust wouldn't make the software quality situation worse. Especially since I'm not convinced that trading memory safety bugs for other bugs alone would be an improvement: At least memory safety flaws are always bugs, so when valgrind, memcheck, stack protector, etc tell us about them during testing or formal methods tell us they exist (which hardly can exist for rust given that the language isn't even formally specified) we always know we have an issue. With general logic bugs it's much harder to be confident that a behavior is a bug.
And, of course, memory safety issues can arise in rust, due to compiler/library bugs or because other restrictions or performance issues have caused users to use unsafe or depend on external library code... so it's unclear how much we can really say that memory safety is in-the-bag. I think to be confident that a critical piece of rust code lacks memory safety bugs we still must test comprehensively, including with fuzzing and we must review its dependencies which are almost always far far far far worse than in C (creating a whole issue for sneaking in bad code).
Informally, my experience with software written in Rust is that it is far more likely to crash (panic) on the spot when I attempt to use it than software written in C-- usually because the author just hasn't bothered with error handling due of some mixture of the language making it somewhat harder to do, out of a culture that doesn't regard code that panics unexpectedly as shoddy work (because unlike a crash in C, it's unlikely to be the sign of a potential vulnerability), or in some cases because the goal was just "rewrite in rust" and the programmer didn't take much care or pride in their work or lacked the subject matter expertise to do it competently.
It's heartening to see this effort has a test suite-- Of course (AFAIK) sudo doesn't have one itself but arguably sudo's test suite is the decades of use it's had in production. A newly written program has a lot more need for a test suite than an established one.
I think rust programs are more likely to have some amount of tests compared to really old free software, but usually not enough to compensate for their newness.
> It's heartening to see this effort has a test suite
A test suite which sudo (here named "ogsudo" although I'm not sure how "gangsta" sudo really is) failed, and as a result sudo's authors had to fix sudo as described in the article we're discussing right ?
> Informally, my experience with software written in Rust is that it is far more likely to crash (panic) on the spot when I attempt to use it than software written in C
Sure, in the C when the author wrote no error handling, it just presses on. Is its internal state now incoherent? Who cares, error handling is for pussies, YOLO. In the Rust it panics. I'd argue that for many cases, including sudo, the panic is much better though still not the ideal outcome of course.
The canonical "Hello, world" program in C and in Rust when given a stdout that claims to be full behaves differently. In C it just exits successfully. Sure, it couldn't actually output the "Hello, world" message, but who checks errors anyway? In Rust it panics, nobody told it how to handle the condition where the output device is full.
> A test suite which sudo (here named "ogsudo" although I'm not sure how "gangsta" sudo really is) failed, and as a result sudo's authors had to fix sudo as described in the article we're discussing right ?
Yup which is table stakes, as the original software didn't have tests (but did have decades of use in production, which is why it has less need for tests than the rewrite).
> Sure, in the C when the author wrote no error handling, it just presses on. Is its internal state now incoherent? Who cares, error handling is for pussies, YOLO. I'd argue that for many cases, including sudo, the panic is much better though still not the ideal outcome of course.
My concern there is that because bad things happen the C program has the error handling, while in rust it may be "don't handle that case? who cares? rust is safe(tm)".
The kind of experience I've had with rust code isn't a case where the c-analog is yolo. You just don't expect widely used C code to crash on some command-line argument misuse or when a file is missing, it's not entirely unheard of but it's not common. In my experience it's extremely common in rustlandia. In that sense, rust culturally has really managed to give me a very strong yolo vibe.
"It can just panic" is no so good when the code is in a library that called from the motor controller for a forklift holding a pallet over your head. ... and in plenty of other less drastic situations.
I agree for sudo most of the time panicing is probably not directly unsafe (though it might produce an outage that causes harm). For software that deals with more complex external state, panicing can still be pretty bad-- like leaving the system in a vulnerable state, leaving confidential information laying around, etc. There are plenty of examples where a simple DOS attack can be used to compromise a system, e.g. DOS a master system to cause a fall over to a slave.
> no so good when the code is in a library that called from the motor controller for a forklift holding a pallet over your head
There should be a mechanical fail safe. The Therac-25's first important difference from prior hardware was that they removed the physical fail safes because hey, nobody will write a program that's incorrect. Yes they will, and it killed people - so stop designing hardware which assumes unreasonable things about software. With an appropriate fail safe if the code panics, the forklift locks up, maybe there's some sort of mechanical reset needed - this is annoying but nobody is dead.
When the firmware in your elevator freaks out, which isn't even a rare occurrence, nobody dies because the hardware people don't trust that firmware. If you let the firmware control it directly, you'd be lucky if anybody used them because they'd be too dangerous. I've watched Third Year undergraduates in electronics learning with (toy) elevators how to write real time software and they were not good at this. The toys were designed so that they weren't damaged when (not if, when) the students smashed the elevator into the top or bottom of the shaft and still kept the motors running.
> My concern there is that because bad things happen the C program has the error handling,
The example we just looked at is common, and in fact the C program doesn't have error handling, it's just ignoring the error because caring about errors is more trouble in C so why bother.
> The kind of experience I've had with rust code isn't a case where the c-analog is yolo
You're going to have to give a lot more details. It's so easy to YOLO in C that it's harder to think of cases you can't just YOLO than cases where you can. C loves functions where if your inputs are invalid you get invalid outputs, which you can then blindly supply to another function where likewise the invalid inputs result in invalid outputs. YOLO all the way.
On mechanical fail safes, they're sometimes impossible, wildly expensive, or simply less safe. In some devices the safe region is a complex polytope defined over a dozen degrees of freedom and may not even be static.
Perhaps for cases where someone will literally be squished when it fails you can draw a bright line rule, but you can turn the safety requirements down some-- instead of being crushed they might just lose a finger, or a million dollar float glass batch will be lost. At some point the risk isn't worth the elaborate hardware failsafe and software will be all you've got. There really isn't a safe threshold in any case: maybe it's just a word processor but its failure costs someone days of work and leads to their suicide. Software reliability matters.
Panicing on exceptional situations is often only slightly more acceptable than crashing or corrupting in other ways (and sometimes less, commonly the corruption is inconsequential).
> caring about errors is more trouble in C
That hasn't matched my experience. YMMV I guess. It's my experience that it's the exception rather than the rule that rust programs won't just panic instead of handling errors sensibly, in particularly because wrapping types to express failure and explicitly handling it along with all the required lifetime management adds a lot of complexity.
So 6 out of 174 CVEs could have been avoided this way. ... but how many more of the unavoided logic errors will be created by using a language which is far more complicated, less clear, and readable/reviewable to far fewer people?
That said, It's a great sign for that the tests that it was was comprehensive enough to find bugs in the original sudo. But a set of tests isn't really complete until it finds a compiler bug too. :)
I think getting a fresh perspective from fresh developers is still a net positive, some of the CVEs are truly embarrassing for such a critical piece of code.
That being said I skimmed the list of CVE and yeah Rust wouldn't have prevented anything. The rust evangelism is exhausting.
On the other hand, part of the goal is to strip out accumulated feature creep. Sudo apparently has a lot of functionality which I never knew existed, all of which could lead to future bugs.
There are quite a few Unix tools which I think could use a similar refresh if not frozen in time thanks to POSIX.
No doubt 80% of the users only use 20% of the features... but do they use the same 20%? :)
I think sudo is often the wrong tool for what people are using it for today-- it's a tool to delegate on a multi-user system. But today people are most often applying it on a single(-ish) user system to raise or lower their permissions.
Ironically, most of the vulnerabilities that sudo has had aren't really of consequence in that modern single user usage: If the attacker can run code as the user, then they'll be able to take over any privilege raising process the user uses even if the 'sudo' tool were flawless.
Once you leave that use case the greater feature set of sudo (including, perhaps some of those crusty features you mention) has more applicability.
The thing to always keep in mind is that the programs cruft is substantially the body of its embedded knowledge. Some of that knowledge might be mistaken or outdated. If you really knew what parts were what-- you could just remove them.
If the rewrite isn't sure it can remove it and just replicates, it may not have understood the function's purpose enough to replicate it faithfully and could even introduce security problems (or cause users to introduce them by forcing them to bodge around configuration statements that no longer work).
In the case of memory safety, we can say that absent compiler bugs, use of unsafe (including by dependencies), or callouts to other languages that rust defiantly eliminates essentially all memory safety bugs.
We can't make the same strong statement about the type system preventing other bugs. The type system in rust requires a LOT more boiler plate code an extra typing, some of which can introduce bugs.
I am not aware of any study that would support the claim that rust reduces the defect rate overall and I'm somewhat doubtful of claims that it does because of the absurd regularity that rust software immediately panics on me when I attempt to use it (and, the amount of firfox crashes I've experienced which of late are almost entirely rust code panicking under the hood). It's also possible that what I'm seeing are all defects that would have existed otherwise made slightly more visible, if so -- it's a good thing.
But we shouldn't let the fact that we can take for granted that rust avoids memory safety bugs by construction from meaning that its other properties which MAY reduce defects over all actually do. Hopefully it does. But writing software entails a lot of cost and risk to undertake for a hope and plenty of things that have been intended to make things better actually made them worse.
That said, personal experience has shown me that modeling data using types (instead of abusing, say, string-to-string dicts as is common in Python, or passing "untyped" strings around for things that /are/ strings but that have higher meaning, like file system paths) is very beneficial to program correctness. Obviously this is doable in many languages, but Rust helps enforce this due to the lack of implicit promotions and general adoption of these idioms in standard/common libraries.
As one specific example, I originally wrote sandboxfs (a FUSE file system) in Go and then rewrote it in Rust -- part because I hit performance issues, and part because I wanted to learn Rust. Trying to reproduce the original structure of the code in Rust was not doable, and having to adapt it to Rust highlighted various logic and concurrency bugs that existed in the Go version but that silently went unnoticed.
That's a good testimonial. Also thanks for the link.
I'm a big fan of strongly typed software, but at the same time I've absolutely seen bugs introduced where people were forced to be explicit in conversions and got it wrong, when the automatic thing would have been the right thing. Maybe the answer to my concern is just that there is no replacement for care and competence and that bad software can be written regardless of the tools.
> but how many more of the unavoided logic errors will be created by using a language which is far more complicated, less clear, and readable/reviewable to far fewer people?
Can we stop pushing this false narrative about Rust being sooooo overly complicated? The real reason this narrative is pushed is because lazy developers don't want to take the time to learn a language that forces them to think. Yes I said it. Most of these developers want to go on autopilot just copying/pasting code from others and not even think about security in the slightest.
Rust is actually really shallow in what you have to learn to write safe code.
For me, languages like JavaScript, python, and C++ are the real complicated languages. Which is why I haven't "learned" (is that even possible) them.
Rust is a dramatically more complex language than C (what sudo is written in!).
If you force the comparison with C++ then your position has more merit, however, I think there is still an argument that the subset of C++ that you must deal with in a given codebase can be (and often is) simpler than the portion of rust you must deal with in any rust codebase. I'm not particularly confident of this position, however, and it's tangential to the point that rust is unambiguously more complex than the language used by sudo.
Aside, there is an enormous amount of thoughtless copy and paste in rust, just as there is in other language. Rust also comes with a culture of extremely promiscuous dependency use, it's not uncommon to build a rust program and watch the compilation download and build two different SSL libraries! -- even a program that you have no interest in using with HTTPS/SSL at all.
Maybe in spite of the bad dependency culture rust actually will mean an advance in software quality in practice. But I think we simply don't have evidence of that (yet), and it's overly hopeful to assume that this will be the outcome simply because it was the intent of the creators of rust.
The fact that any criticism or concern gets mobbed an that there is so much mindless advocacy from people that haven't even considered issues like the trusted-computing-base problem doesn't speak well for the prospects that rusts' own problems that limit its benefit will be addressed.
Which is why I didn't mention C along with those other languages that are spaghetti. C is simple, clean, and won't get in your way. That last part is why most serious bugs occur. Unless the language is built from the ground up to prevent the bugs from compiling then you will always have vulnerabilities.
I know that C is a beautiful language and Rust is ugly but at some point we have to realize that humans are error prone and will always produce buggy C code.
As for Rust dependency issues, here's a very simple article for beginners:
Duplicates aren't the main cause of dependency hell in Node.js, although they certainly don't help matters.
Other systems languages have kept simple, C-like syntax without the associated error rates. Removing a lot of implicit casting and having sane compiler standards is all you need, not a bad implementation of the HM type system.
sudo doesn't have 174 CVEs. People always link to a simple mitre keyword search, and many just mention sudo in passing; you don't have to look far: the two first items on that list are not about sudo at all.
The last time I looked at this[1] I found the most serious problems were logic issues, and not memory issues (although there are some of those a well).
> using a language which is far more complicated, less clear, and readable/reviewable to far fewer people?
By that reasoning we should rewrite all these tools in JavaScript or Ruby, Python or some such. Clearer (than C), more readable and readable/reviewable to far more people.
Edit: to be clear: I think that would be an extremely dumb idea.
> By that reasoning we should rewrite all these tools in JavaScript or Ruby, Python or some such. Clearer (than C), more readable and readable/reviewable to far more people.
> Edit: to be clear: I think that would be an extremely dumb idea.
I dunno if it really is that dumb.
Why can't sudo be rewritten in Go? The performance difference is not likely to be noticed and there's no runtime platform dependence (as with Python, et al).
And, with managed memory, you'll have even fewer memory-safety bugs than with Rust.
What exactly makes Rust a better fit for sudo than Go, bearing in mind that Go is a whole lot safer than Rust.
Seriously, anytime you have to manage memory yourself it's going to include the danger that you mismanage it. Rust does not prevent all types of memory mismanagement.
What's more, the burden is on the programmer to manage memory; all the Rust compiler tells you is where there are potential (not actual) memory errors. The programmer still has to fix them, and even with that there are still a number of memory errors that will get through.
Compare to a GC - allocate an object and then forget about it. No problems at all.
All of JS, Ruby, and Python are vastly more complicated languages than C. They do have a shallower learning curve. It's unclear to me what degree they have a bigger review audience: there are an awful lot of write-only JS programmers! :)
I wouldn't trust Googlers as a measure of any sort of quality, given their love of Go.
C code relying on tons of macros will be harder to read then C code that delegates preprocessing to another language. Rust code that leans heavily into the ML features and unwrap.parent.unwrap.parent.unwap... will be a lot more unpleasant than Rust code written to just get the bit shifting job done. I imagine it really depends on the code base.
You might be overestimating the popularity of Go within Google.
That said, a tool like sudo should be written almost entirely in a very high-level language that is easy to use and review. C, C++, or even Rust should be reserved for places where the system interface requires it, if there are any such points of interface.
On the flip side, languages like C,C++ and Rust have the major benefit of having next to no runtime component to it, allowing trace-driven fuzz testing to achieve a much higher level of confidence in its test coverage. For a tool like sudo, this can matter a lot.
I think this is really important and it's a big part of what makes it unrealistic to write critical software in Python or Java: It's too slow to get extremely deep testing, even with fancy tricks like snapshotting the execution state to avoid startup costs.
But, that said, Rust code compiled in debug mode which required to get integer overflow detection is slow enough that it severely degrades the ability to use fuzz testing on many codebases, FWIW. I believe the reason is that debug mode always disables numerous optimizations that are required to make rust performant at all because of all the boilerplate emitted by earlier stages of compilation.
AFAIK there isn't a way to get the equivalent of GCC's "-fsanitize=undefined" (or -ftrapv) for checking for unexpected overflows at a performance cost similar to "-fsanitize=undefined" performance cost on C code.
It's still a much better situation than python or java, I think-- but an area that could use improvement which won't be improved if rust is above criticism.
You can pretty trivially turn on overflow checks yet compile with all of the other release options as normal. It's one line in cargo.toml or one flag to rustc.
I would probably use the flags with RUSTFLAGS to make sure that it goes to all dependencies and not just the crate you're building, if what you're building has dependencies.
Huh? Go has its tradeoffs, some people don't like it and that's fine. But nobody can deny that it is by far one of the most 'readable and reviewable' (the argument in question) mainstream languages.
The praise of Go didn't end with it being just readable, but also 'brutally pragmatic' and all that.
If Googlers think that Rust code is just as readable as Go, even though Rust obviously makes tradeoffs against readability for other features, I would be tempted to mark that as Googlers just having a culture about being overeager to think the tools they are using are the best.
Why can't I deny that it is readable and reviewable? I don't think it is, basically at all. It gets evangelized as if it is but I have yet to see a compelling argument that can convince me. I've had the displeasure of having to dive into some codebases and I specifically hate it for that reason.
Well, I can't speak for you, of course. I also haven't seen that code you reviewed. And I won't try to evangelize you. I'd say, in a nutshell, that Go is readable and reviewable because its grammar consists of the C-style basis that most every imperative language has, and not much more. It's the lack of features that makes it very 'WYSIWYG'.
Solaris had a bit of Java in it, and it was a terrible fit on account of how slow the JVM startup was and how large its memory footprint. Now that was almost 20 years ago and by now there's ways to optimize the JVM for short-lived processes, but it's still a pain and it's still a huge installation just for a small program like `sudo`. No, something like `sudo` needs to be natively compiled, or if bytecompiled then to a tiny VM -- tiny in every way: tiny opcode set, tiny bytecode interpreter, tiny install size.
Memory safety is just the tip of the iceberg of issues with sudo. Repeat after me - sudo is not a security tool. Maybe if you stick with a tiny subset of it then yes. But that subset would be better off being its own program (doas).
This is a lot of SLOC, and a huge surface area to pull targeted supply-chain attacks, IMO.
P.S.: I know you don't compile in all of this into the binary, yet consider the eyes and work hours required to verify that the whole chain is sane and safe.
This is how it looks: