Testing the memory safe Rust implementation of Sudo/Su(ferrous-systems.com) |
Testing the memory safe Rust implementation of Sudo/Su(ferrous-systems.com) |
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:
vagrant@rust-playground:~/development/sudo-rs$ cloc .
3549 text files.
3271 unique files.
453 files ignored.
1 error:
Line count, exceeded timeout: ./vendor/proc-macro2/src/parse.rs
github.com/AlDanial/cloc v 1.86 T=9.67 s (320.2 files/s, 208065.8 lines/s)
--------------------------------------------------------------------------------
Language files blank comment code
--------------------------------------------------------------------------------
Rust 2637 49276 111413 1754298
diff 2 884 32618 35892
Markdown 182 4903 9 13341
TOML 137 807 1073 5734
Assembly 8 90 71 1244
YAML 5 80 26 393
JSON 106 0 0 120
reStructuredText 1 70 4 90
C/C++ Header 9 5 2 79
Bourne Shell 5 14 17 64
C 2 6 6 46
Bourne Again Shell 2 7 8 41
Python 1 17 19 38
Dockerfile 1 0 0 2
--------------------------------------------------------------------------------
SUM: 3098 56159 145266 1811382
--------------------------------------------------------------------------------If I remove those the line count is about 100k, which is about the same as sudo.
I didn't know that Rust programs run without a libc.
~/Code/tempfile !! tokei vendor
===============================================================================
Language Files Lines Code Comments Blanks
===============================================================================
GNU Style Assembly 8 1405 1276 39 90
C 1 3 2 0 1
Shell 1 23 18 1 4
TOML 25 1536 1178 234 124
-------------------------------------------------------------------------------
Markdown 29 2193 0 1560 633
|- C 1 2 2 0 0
|- Rust 11 277 220 19 38
|- TOML 8 60 58 0 2
(Total) 2532 280 1579 673
-------------------------------------------------------------------------------
Rust 917 811953 792140 4536 15277
|- Markdown 304 17252 124 14364 2764
(Total) 829205 792264 18900 18041
===============================================================================
Total 981 817113 794614 6370 16129
===============================================================================
[1]: https://github.com/memorysafety/sudo-rs/blob/60985b2f5f7ffa8...(by the way, Herman, much belated thanks for putting together the original Rust LA meetup all those years ago!)
If you can pull a lower level attack with general purpose toolchain, targeted for either implementation, it's a more impressive feat, for sure.
However, Rust implementation adds a significant SLOC on top of that complexity.
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:
0.0358 secs
┌───────────────────────────────────────────────────────────────────────────────────────┐
| Language files size blank comment code |
├───────────────────────────────────────────────────────────────────────────────────────┤
| Bash 3 939.00 B 7 2 30 |
| C 1 1.31 KB 5 6 44 |
| C Header 1 226.00 B 0 0 7 |
| D 15 31.75 KB 32 0 143 |
| JSON 22 39.69 KB 0 0 22 |
| Markdown 16 53.46 KB 425 0 1054 |
| Rust 396 4.98 MB 13852 9502 131650 |
| Shell 5 2.24 KB 11 18 50 |
| Toml 14 9.60 KB 54 61 319 |
| Yaml 2 10.14 KB 70 0 341 |
├───────────────────────────────────────────────────────────────────────────────────────┤
| Sum 475 5.13 MB 14456 9589 133660 |
└───────────────────────────────────────────────────────────────────────────────────────┘
As a comparison, this is the output for https://github.com/sudo-project/sudo: 0.0439 secs
┌───────────────────────────────────────────────────────────────────────────────────────┐
| Language files size blank comment code |
├───────────────────────────────────────────────────────────────────────────────────────┤
| Autoconf 124 1.90 MB 2618 4317 59031 |
| C 365 4.20 MB 15977 22626 111340 |
| C Header 90 1.14 MB 1816 4911 18803 |
| JSON 7 9.22 KB 0 0 236 |
| Markdown 10 133.62 KB 676 0 2498 |
| Pascal 3 33.63 KB 79 0 925 |
| Perl 2 12.81 KB 54 83 306 |
| Plain Text 1 15.00 B 0 0 1 |
| Protocol Buffer 2 5.54 KB 22 0 185 |
| Python 10 26.41 KB 152 259 295 |
| Shell 77 358.96 KB 1589 2534 8961 |
| Yaml 4 7.98 KB 16 38 205 |
├───────────────────────────────────────────────────────────────────────────────────────┤
| Sum 695 7.81 MB 22999 34768 202786 |
└───────────────────────────────────────────────────────────────────────────────────────┘
It should be noted that the sudo project's dependencies and autogenerated code aren't included in this overviewedit actual counts:
$ cargo vendor && cd vendor && {
for p in * ; do
echo -n $p
tokei $p | rg '^\s+Rust'
done } | sort -n -k 4 | tabulate
---------------------------- ---- --- ------ ------ ---- ----
errno-dragonfly Rust 2 9 8 0 1
windows_aarch64_gnullvm Rust 2 11 9 0 2
windows_aarch64_msvc Rust 2 11 9 0 2
windows_i686_gnu Rust 2 11 9 0 2
windows_i686_msvc Rust 2 11 9 0 2
windows_x86_64_gnu Rust 2 11 9 0 2
windows_x86_64_gnullvm Rust 2 11 9 0 2
windows_x86_64_msvc Rust 2 11 9 0 2
winapi-i686-pc-windows-gnu Rust 2 25 13 12 0
winapi-x86_64-pc-windows-gnu Rust 2 25 13 12 0
windows-targets Rust 1 54 46 3 5
output_vt100 Rust 2 67 55 0 12
cfg-if Rust 2 164 131 16 17
instant Rust 4 316 260 6 50
countsctor Rust 2 331 254 21 56
errno Rust 5 375 280 41 54
diff Rust 4 561 485 9 67
autocfg Rust 9 702 558 41 103
yansi Rust 7 741 627 3 111
signal-hook-registry Rust 3 818 566 150 102
fastrand Rust 4 830 710 16 104
hermit-abi Rust 4 847 601 5 241
pretty_assertions Rust 5 1231 1072 33 126
glob Rust 2 1589 1291 113 185
bitflags Rust 20 1715 1373 105 237
unicode-ident Rust 11 1794 1697 36 61
signal-hook Rust 17 1969 1520 147 302
tempfile Rust 15 2367 1928 102 337
quote Rust 17 2458 1979 148 331
redox_syscall Rust 23 3595 2996 83 516
log Rust 9 3635 2970 97 568
io-lifetimes Rust 15 4218 3605 80 533
proc-macro2 Rust 17 5286 4514 139 633
cc Rust 13 5861 4767 488 606
rustix Rust 236 39927 33837 1467 4623
syn Rust 92 51956 48946 493 2517
libc Rust 224 109836 99688 2073 8075
linux-raw-sys Rust 61 145628 145455 84 89
winapi Rust 405 179933 176630 3299 4
windows-sys Rust 281 497624 497608 4 12
---------------------------- ---- --- ------ ------ ---- ----
[0]: https://github.com/memorysafety/sudo-rs/blob/60985b2f5f7ffa8...https://github.com/memorysafety/sudo-rs/blob/60985b2f5f7ffa8...
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.
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.
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.
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.
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).
https://github.com/slicer69/doas/
However unlike sudo and opendoas this does not implement the persist feature on not-OpenBSD.
Is there something wrong with sudo?
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.
Sudo before 1.9.5p2 is memory unsafe.
Sudo before 1.8.26 is memory unsafe.
Sudo before 1.6.6 is memory unsafe.
Source: https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=sudo
Am I missing something?
sudo is 43 years old though
> It is written in the extreme YOLO style by people with very poor taste.
Sounds a bit exaggerated to me. Do you happen to have data on this?
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...
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. :)
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.
Sure other language can do better here too, but you’re not just getting memory safety from Rust.
I think you are misremembering the statistic.
It's 70% of security bugs that are related to memory safety: https://www.zdnet.com/article/chrome-70-of-all-security-bugs...
The way you say it, it means that out of every 100 bugs, 80 are due to memory safety.
The reality is that out of every 100 security bugs, 70 are due to memory safety.
For example, a codebase with a thousand bugs might only have 10 that are security bugs, of which 7 are due to memory safety.
You imply that a codebase with a thousand bugs have 800 memory safety issues.
That being said I skimmed the list of CVE and yeah Rust wouldn't have prevented anything. The rust evangelism is exhausting.
Many logic error were discovered in real life use cases over many years
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.
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.
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.
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).
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.
> 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.
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.
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.
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.
1. on Linux
2. in embedded
The vast majority of them do use libc, as the underlying platform requires. The default on Linux is to use a libc as well.
If you look at the linked Cargo.toml you'll see
[dependencies]
libc = "0.2.139"
<snip>
[dev-dependencies]
pretty_assertions = "1.3.0"
tempfile = "3.5.0"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:
https://marketsplash.com/tutorials/rust/rust-dependencies/
There's also the cargo tree command to find duplicate dependencies:
cargo tree --duplicates
By far more better than the dependency hell that is Node.js :D
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.
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.
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.
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.
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.
As a general point I agree with you, but both sudo and sudo-rs are pretty large.
I don't believe I've ever seen it shared in the context of rewrite-in-rust, but I think the views it expresses also have some value in this context.
There are quite a few Unix tools which I think could use a similar refresh if not frozen in time thanks to POSIX.
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 progress. Looks good so far. I haven't tried it, though.
The great grandparent was talking about audit for supply chain attacks.
dev-dependencies can run on dev machine in compile time
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
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.
By the magic of find-in-page:
1.9.5p2, CVE-2021-3156
1.8.26, CVE-2019-18634
1.6.6, CVE-2002-0184
1.8.0-to-1.9.12, CVE-2022-43995
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.
I'm not seeing that in the list. Of the listed CVEs, not many of them are due to memory safety.
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.
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).
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.
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.
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.
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.
> 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.
That's my point, sudo survived 43 years, had to support a myriad of different platforms and configurations, doas was started in 2015.
incidentally OpenDoas is written in C too.
We'll see in 35 years how doas is doing.
Writing security-critical software entirely in C is prima facie evidence of a disqualifying lack of good taste.
> 1.8.26, CVE-2019-18634
> 1.8.0-to-1.9.12, CVE-2022-43995
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.
OP was wildly off the mark.
Some bugs aren't as important as others, but they're all just bugs.
That's an unreasonably wide and rather useless definition of "security bug".
> Some bugs aren't as important as others, but they're all just bugs.
I think nobody questioned that bugs are bugs :)
For security software, maybe.
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 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.
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.
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.
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.
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.
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.)
has one of these bugs ever been exploited?
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.
If you use too much stack space, it terminates the program. It does now however allow for arbitrary code execution like most C compilers do.
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.
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.
[profile.release]
overflow-checks = false
in cargo.toml.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.
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)".
It's not necessarily the case that both are yolo. I'm sure you've heard of risk compensation. https://en.wikipedia.org/wiki/Risk_compensation
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.
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.
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.
It's a joke.