6 ms·
This is why separating return values from error codes is important. For example, in Rust, you’d never get into this situation, because a decent fork ffi functi
by bluejekyll 7y ago
This is why separating return values from error codes is important.
For example, in Rust, you’d never get into this situation, because a decent fork ffi function would immediately convert -1 into a Result carrying an error, and properly check errno. Java and C++ would throw an exception, etc.
Thus preventing all sorts of bad behavior up the stack.
- jacobush 7y agoYeah, but in C++ you get people ignoring exceptions too. I'm fine with return codes, I don't mind them even though exceptions are maybe a little bit better.
- swsieber 7y agoTrue, but in this case, ignoring an exception would be better than ignoring a return code, right? It prevent you from accidentally sending a signal to -1 (e.g. all the processes you can). The danger here with the return code is that the return code value is a valid (but not intended) input to another function that normally uses the normal return value.
- jacobush 7y agoTrue, I guess just this example didn't jive with me. I'd never dream of ignoring the return code of fork(). Some other call I don't respect enough would probably bite my ass, though.
- oconnor663 7y agoIt's not just a question of whether you did the right thing when you wrote the code. Code gets refactored and moved around, maybe by people less experienced than the original author, or maybe just by the original author when they're tired or after they've forgotten the context. Wrapper functions get defined, libraries get updated, and support gets added for new platforms. Each of those changes is an opportunity to accidentally stop checking the return code.
- akavi 7y agoHence why `Result` is really valuable. Can't ignore it (plus, the semantics are straightforward)
- thestoicattack 7y agoIf by ignoring, you mean non catching anything, I'm often okay with that. If an exception makes it top-level, you'll get a std::terminate and be done. The "canonical" example people use a lot is std::bad_alloc. Often, there is no point in catching it -- what cleanup or fallback work are you planning to do when you can't even allocate memory? Of course, silently swallowing exceptions with an empty catch-block is terrible.
- masklinn 7y agoIf you ignore (as in fail to handle) an exception, your software dies safely, it won't do the wrong thing. If you ignore or misuse error code, you carry on in a corrupted state.
- icedchai 7y agoHave you seen old java code that has tons of empty try / catch blocks? It is very, very common.
- lolc 7y agoJava is special in this regard because the common case of letting an exception terminate the program is harder than ignoring it. When I was still writing Java, I'd gotten so annoyed by this that in the top layers every method "throws Exception". It was less verbose than promoting all the checked exceptions to runtime errors. Unfortunately people don't know about this possibility and write empty catch statements because "it's not going to happen anyway". Yeah right.
- jcelerier 7y ago> Yeah, but in C++ you get people ignoring exceptions too ... and then you can just do `coredumpctl gdb` and start debugging right where things went wrong ?
- swsieber 7y agoI cannot upvote this enough. I've written some rust code which uses fork, and after reading this, I went to check the docs. And lo and behold it follows the convention you put forth. So I was safe. Yay Rust, where things either fail (panic and quits the program) or returns a result that you can't use until you check for the error.
- the8472 7y ago> So I was safe. Fork (without exec) is a very sharp tool because it can violate ownership, doubly so in multi-threaded programs. So just because you get a Result from it doesn't mean all its pitfalls are handled.
- bluejekyll 7y agoIt’s true, though the FD_CLOEXEC flag generally helps in this area. Recently though I had to remove a bunch of these safeguards because I wanted to share handles and such across processes. So sometimes it’s nice to have that feature.
- hinkley 7y agoCorrect me if I'm wrong, but isn't there a problem with mutex sharing as well?
- the8472 7y agoI was using a wide definition of "ownership". Parent and child unintentionally sharing a mutex is one manifestation of those issues.
- tedunangst 7y agoNote that posix says you shouldn't do anything except immediately call exec after fork in multithreaded programs.
- klodolph 7y agoThis is not true. IIRC, you can do anything as long as you don’t call any function which is not async signal safe. Otherwise, it would be very difficult to do simple things like run a child process in a separate directory (with chdir()), or report a failed exec() to the parent. The typical way to report errors upwards is to create a pipe with CLOEXEC and then write the error code to it from the child if exec fails. This requires at least calling write(). You may be remembering vfork(), which is a bit of an obsolete API.
- lima 7y agoGo also cleanly separates results and errors. syscall.ForkExec returns an error if the call failed.
- OskarS 7y agoAll that is certainly true, but we're talking about a function that dates back to the 70s here. It predates all of those nice error handling concepts (exceptions, Result<T>, multiple return values), so it's pretty sensible from that perspective to have -1 be the error value. But yes, don't do this today, obviously.
- catblast 7y agoI wonder how a rust compiler implementation would fare on a PDP-11/20
- bluejekyll 7y agoWe're still dealing with this today as so many of us deploy and develop software for POSIX that exposes us to these edge cases. So, yes, while working in Rust and using better APIs designed around these interfaces, we won't do the wrong thing, but there's still a lot of people that are going to end up getting cut by these old 70's interfaces. It would be nice if we didn't have to work with these, but many of us still do.
- masklinn 7y ago> All that is certainly true, but we're talking about a function that dates back to the 70s here. It predates all of those nice error handling concepts (exceptions, Result<T>, multiple return values), so it's pretty sensible from that perspective to have -1 be the error value. It doesn't predate creating a struct which tells you exactly whether thing went wrong and how.
- the_mitsuhiko 7y ago> For example, in Rust, you’d never get into this situation … because you cannot fork() safely in Rust.
- JyB 7y agoYes it's just a good practice when exposing an API. It's language agnostic.
- zyxzevn 7y agoUsing integer codes like 0 and -1 were the old style of returning errors. Often combined with some undocumented features. It is really wrong, but that is caused by the C-functions returning only one integer of data. And the bad C libraries. Null used to be -1 in some compilers. Mixing pointers as integers is a recipe for disaster, not compatible with some CPU-s, but it is C-standard. In the windows32 API they improved it a bit, with functions that only returned an Error code. They required struct addresses in which results were placed. I think that the process structure is also a lot better than the fork() function.
- kabdib 7y agoIt's even more fun when you're mixing libraries, and some of them return 1 on success, some of them return zero, and some -1, and you can't tell by inspection which is which. Bonus points if the API you're using inconsistently mixes boolean, null-pointer, HRESULT, DWORD, and returning a status in a value you pass a pointer to (you did initialize that value, right?) Why do we keep doing this to ourselves?
- dmitrygr 7y agoWhat are you talking about? They are perfectly separated. Top bit of the result IS the separation. And do you know why you'd never get into his with rust? Because rust doesn't support fork.
- pcwalton 7y ago> What are you talking about? They are perfectly separated. Top bit of the result IS the separation. "Separation" in this case implying "hard to misuse", which the POSIX fork() API certainly is not. > And do you know why you'd never get into his with rust? Because rust doesn't support fork. Yes, it does. https://docs.rs/nix/0.17.0/nix/unistd/fn.fork.html https://docs.rs/nix/0.17.0/nix/unistd/fn.fork.html
- jimbob45 7y agoWouldn't wrapping potentially dangerous function calls as monads in C++ solve this exact issue?
- asveikau 7y agoI don't know Rust well, but my understanding is the Rust way is better than Java or C++. Exceptions are a mess. Either you go the Java route of tagging everything as throwing every subclass of Exception under the sun (which encourages people to write empty catch blocks to silence a noisy compiler), or you go the C++ route where you are not totally clear an error can occur when writing the code or from glancing at it. (Combine with operator overloading for most confusing results.) Having an error result that can perhaps be easily propagated to the caller is the best of both worlds, and I think is the thing that good C code tries to approximate in a more manual way.
- toast0 7y agoSystem calls actually do return the result seperately from success or failure. It's libc that smooshes them into one thing. Specifics vary by platform, but for FreeBSD i386, the result usually comes back in register EAX (but docs say sometimes another register), and the success or failure comes back as the carry flag. Of course, C never made access to the carry flag easy, so libc smooshes things together.
- nneonneo 7y agoSystem calls can return them separately, but don’t have to. On Linux a failing syscall returns -errno; the syscall wrapper assumes any return value in the range -1 to -4095 is an error, stashes the positive error number in the C errno variable and translates the return value to -1.
- tyingq 7y agoposix_spawn() is reasonably portable and returns them separately.
- klodolph 7y agoIt’s also not a system call :-/ at least with Linux/glibc. Although this is not really the point of the discussion, I’m not aware of system calls that return their result separately the way that posix_spawn does. They obviously could.
- gameswithgo 7y agoA similar "sigil" related bug was the cause of the sudo exploit a few weeks back. Option types are often sold as a way to end the dreaded 'null pointer exception' but I think ending the dreaded "sigil we forgot to handle" benefit may be bigger.