5 ms·
I always find it a bit unfair when I see sloppy C programs used for shock value. What if the Rust developer uses fork().unwrap_or(default_value) in a hurry, or
by zuzun 11y ago
I always find it a bit unfair when I see sloppy C programs used
for shock value. What if the Rust developer uses fork().unwrap_or(default_value) in a hurry,
or writes
if let Some(child) = fork() {
do_only_child_stuff();
} else {
do_only_parent_stuff();
}
or
if let Some(ForkResult::Child) = fork() {
do_only_child_stuff();
} else {
do_only_parent_stuff();
}
Now, if you're about to tell me that the examples above are totally stupid and no
developer would do such a thing, then you know how I feel about the sloppy
C versions. Doing a system call and not checking for error is totally
stupid as well.
By the way, you can also write your own wrapper functions in C,
that transform the return value into something like
struct fork_status {
enum { ERROR, PARENT, CHILD } state;
int ret;
};
Then Clang and GCC will warn you about missing switch cases.
That said, the libc bindings in Rust are pretty low-level and a project that
offers higher-level wrappers can be very helpful, so I hope my comment doesn't
create the impression that I'm ripping on the project itself.
- masklinn 11y ago> What if the Rust developer uses fork().unwrap_or(default_value) in a hurry The point here is that the language's tools and APIs can significantly better drive the developer towards the safe/right solution, that's a large point of type theory and static type systems after all. In this case rust's type system is used to split out the various "result cases" and notify the developer upfront of the various cases to handle. The return type pretty much tells you how the function will behave and what you need to take care of as the caller. That aside, why would you unwrap_or(default_value) if you're in a hurry when unwrap() is shorter (and you can later grep for "unwrap()" to find dodgy/hurried code, whereas unwrap_or is a perfectly legitimate recovery strategy). > Now, if you're about to tell me that the examples above are totally stupid and no developer would do such a thing, then you know how I feel about the sloppy C versions. Doing a system call and not checking for error is totally stupid as well. The issue being that even though you have a compiled statically typed language it's of absolutely no help in "checking for error", and interactions between syscalls can be hard to predict, not checking for fork(2)'s error isn't the end of the world... until you pass its result to kill(2) for instance (it might also give strange results if you pass specific pids to waidpid)
- sickbeard 11y agoThis has been proven to be wrong over and over again with the many variations of languages that came after C. Newer "safer" languages don't improve bugs
- kod 11y agoWhere's this "proof" you speak of? By contrast, strong static typing does in fact prove the absence of certain errors.
- nickpsecurity 11y agoOn contrary, empirical studies done in the 90's and such by the likes of Mitre Inc showed people using Ada and C++ were more productive than C developers while producing way less defects. That the language and libraries were designed specifically to counter hard-to-track issues were one of the reasons why. That languages would improve safety was known far back as MULTICS with PL/0 where it's prefixed strings and reverse stack prevented two of the most common crashes/hacks in UNIX/C land. The evidence is on our side that well-design language significantly reduces number of defects in production code if other variables are equal.
- pcwalton 11y agoWhere are all the use-after-free bugs leading to remote code execution (for example) in projects written in languages other than C or C++?
- joehillen 11y agoThat is blatantly false. Type-safety can prove (as in a mathematical proof) that whole classes of bugs are impossible.
- lmm 11y agoThe difference is that in C the bad way is often the most concise/easiest way. So e.g. snippet examples tend to not check for errors, and the language guides you towards not checking. http://www.haskellforall.com/2016/04/worst-practices-should-be-hard.html http://www.haskellforall.com/2016/04/worst-practices-should-... . IIRC C simply doesn't have a nice, standardized way to do "compose several possibly-error calls, resulting in either a single error or the successful result".
- Manishearth 11y agoThe point is that nix-rs doesn't let you kill all the processes by accident, you have to be trying to do it in Rust. You also have to explicitly ignore the error. Whereas in C it's pretty easy to forget to check for the error and subsequently crash everything. Sure, you can have the same higher level API in C. Though Rust will not compile if there's no exhaustive match, and you cannot access the `int ret` if it's in the wrong state, but it's close enough.
- barosl 11y agostruct fork_status { enum { ERROR, PARENT, CHILD } state; int ret; }; I think the point of tagged unions (as in OP's example) is that you cannot access `child` if the call failed. Your example does have an exhaustive check (by a compiler warning), but it doesn't prevent you from misinterpreting `ret` as a pid. Does C have a method to enforce this restriction?
- justincormack 11y agoWell pids should be pid_t not int. System calls should really return two values, return value and errno, but C doesn't handle that very well either.
- masklinn 11y ago> Well pids should be pid_t not int. pid_t isn't a newtype, it's just a typedef, so that doesn't really make a difference. > System calls should really return two values, return value and errno, but C doesn't handle that very well either. Most system calls want to return either, not both. And C doesn't handle that at all.
- sitkack 11y agoAppropriate tangent: Remember that part where null terminated strings save at most three-bytes per string? And if the allocator is long word aligned it saves none-bytes. The first to arrive sets a disproportionate amount of the direction.
- usefulcat 11y agoNot sure how you get that. Let's assume that memory is allocated in sizes that are multiples of 4, and length-prefixed strings have a 4 byte length prefix. str len: c-str bytes: size-prefix bytes: difference: 1 1+1 (4) 4+1 (8) 4 2 2+1 (4) 4+2 (8) 4 3 3+1 (4) 4+3 (8) 4 4 4+1 (8) 4+4 (8) 0 5 5+1 (8) 4+5 (12) 4 6 6+1 (8) 4+6 (12) 4 7 7+1 (8) 4+7 (12) 4 8 8+1 (12) 4+8 (12) 0 So, for example: "ABCDE" as a C-style string would require 5 bytes for the string plus one for the null terminator, which would be satisfied by an allocation of 8 bytes. An equivalent length-prefixed string would require 4 bytes for the prefix plus 5 for the string, which would then be rounded up to 12. The only time the size-prefixed variant doesn't require more memory is when the string length is a multiple of 4. So the length-prefixed version requires an additional 3 bytes on average.
- agentultra 11y agoIt's trivial to write an incorrect program in C because its a language that expects you, the programmer, to know what you're doing. I assume that if you're going to use the `fork()` and `kill()` syscalls on your OS that you're going to read about them and consider their consequences... not write the first trivial example that comes to mind and ship it. An example program written like that is not endemic to C or a logical consequence of its design. It's ignorance. Such examples frustrate me too. It's practically a straw-man argument. > By the way, you can also write your own wrapper functions in C And your own abstractions to provide more safety guarantees... it really depends on your judgement to balance risk, reward, and goals. It's possible to write a system that does all of the checks for you but you'll sacrifice the time and energy to do so (and will still find users blowing off their feet regardless of your best efforts). Rust is doing very cool things but it's not going to make systems programming magically easier. Someone still had to write nix and you should still understand your programs.
- cmrdporcupine 11y agoI often see these precise points in code review arguments, by developers doing something 'risky' in their code: "It's only risky if you don't know what you're doing." Half the time the same developer has made a mistake in the same code. With software being so complex, so full of human error -- almost any tool and practice that can help remedy this situation is welcome in my books.
- agentultra 11y agoProgramming is hard because thinking is hard. I think it's entirely reasonable to write security-focused software in a highly restricted language with concrete formal semantics. The problem with trivial examples is that they're often constructed to prove a point and don't represent what a real developer would write if they were to seriously consider the problem in C.
- oldmanjay 11y agoTell that to the silly mistakes I've made a career out of fixing (many of them mine!)
- kalmar 11y agoAuthor here. This is a really valid criticism of the post, and I think it'll inform the next post I write on the topic. Thanks!
- aidenn0 11y agoI think the C code presented and the rust code presented are approximately equal in their levels of quick and dirty. Using "expect" is the quick and dirty way of ignoring the error case in Rust, and doing nothing is the quick and dirty was of ignoring the error in C. Perhaps "unwrap" is more quick and dirty, but it still fails much faster and with less damage than the quick and dirty C code.
- laughinghan 11y agoI don't think this is unfair. In order to fail as badly as the sloppy C program, the Rust developer would have to explicitly pass in -1 as the default_value, like fork().unwrap_or(-1), right? The developer might do that anyway, but that's certainly an improvement over -1 happening implicitly; they'd have to think about it, and alarm bells would go off as soon as they re-read the kill() manpage, rather than only upon re-reading both the kill() and fork() manpages. Similarly, neither of your "if let Some() = fork()" examples would fail as badly as the sloppy C program, right? All the examples you could come up with of sloppy Rust code fail less poorly or make it harder to fail as poorly as the sloppy C code, and you were deliberately trying make your Rust code sloppy, whereas the sloppy C code was based on examples from the wild: http://rachelbythebay.com/w/2014/08/19/fork/ http://rachelbythebay.com/w/2014/08/19/fork/ Still seems like a good argument that Rust's design is an improvement.