Disagree - the unfortunate interaction with kill is not clearly documented.
Do you implement error checking when calling printf, unlike every C codebase I've ever encountered which uses it? If not, you've implicitly acknowledged some error cases just aren't worth handling, or useful to handle. The question is then - when is it important?
PSAs like this one make it clear where the documentation may not have: Error handling fork() is important, and unlike error handling malloc where free(nullptr)ing later is a safe noop, kill(-1)ing later is an unsafe hazard that must be avoided. Additionally, it's frequently the case that the documentation is poor and would not help you even when you do bother to read it. Here's me previously ranting that the vast majority of documentation about atoi fail to clearly and adequately call out that atoi("a") is undefined behavior and citing my sources: https://news.ycombinator.com/item?id=14861447
> so how could someone fall under the impression that this wasn't the case?
Continuing past my atoi example...
Maybe they looked at alternative, poorer documentation. Maybe they looked at poor example code that didn't bother with error handling. Maybe they looked at decent documentation that failed to adequately stress the importance of error checking (EDIT: I'd argue this includes your wikipedia example). Hell, maybe they looked at great documentation - about a specific platform's implementation of fork, which perhaps makes fork() failing fatal to the calling process and thus "infalliable". Maybe they looked at the documentation for their favorite language's wrapper of fork, which throws an exception instead.
Maybe they didn't look at the documentation at all.
Maybe they learned of fork through word of mouth when the internet was down on a system without manpages. "Can fork ever fail?" "Hmm... I've never seen it fail." "Good enough for me!"
Perhaps this lack of knowledge can only come about by foolishness - but human nature and statistics mean at least one of your generally smart coworkers has probably fallen prey to such foolishness.
There's a reason that Unix sends EPIPE to a process when writing to a broken pipe, and why the default handler for SIGPIPE is to terminate the process. Interestingly, the Rust runtime blocks SIGPIPE, which was a naive and dumb thing to do [1], but which is now impossible to undo.
Similarly, in C the fail flag for FILE objects is persistent to permit alternative error management strategies. This is even carried over into Go, AFAIU. Basically, it's okay to leave a series of I/O statements unchecked so long as you check at the end of the block or transaction, or at the very least at close time.
The printf case is a bad example because the inconvenience of checking for failure on every call has already been accounted for.
I don't think I've ever seen C code that fails to check fork for an error condition, though I'm usually only ever reading my code and the code of widely used open source projects.
[1] Considering how much Rust touts the ease of FFI and integration with C and C++ projects.
As an author and consumer of cross platform software I can't rely on that - no SIGPIPE on windows - not to mention it's a pretty coarse hammer. Return value + errno is the way to go if I actually care about the exact behavior. One can wrap the call and panic/abort/terminate, but most people don't bother to do that, nor can I particularly blame them for not bothering (especially when I generally don't either.)
> I don't think I've ever seen C code that fails to check fork for an error condition, though I'm usually only ever reading my code and the code of widely used open source projects.
I'd be curious if git blame shows they had that error handling from day 1, or if it was generally added in response to heisenbugs. My money is on the latter, but perhaps you're generally exposed to better codebases than I am.
But shell programming and process pipelines aren't common on Windows. Rust shouldn't have changed the default behavior. SIGPIPE serves a critical role on Unix by ensuring that processes don't keep running when their consumers disappear.
> Return value + errno is the way to go if I actually care about the exact behavior. One can wrap the call and panic/abort/terminate, but most people don't bother to do that, nor can I particularly blame them for not bothering (especially when I generally don't either.)
Fortunately in Rust you're usually forced to handle the failure condition. The problem is with FFI to existing libraries that implicitly rely on default Unix semantics.
> I'd be curious if git blame shows they had that error handling from day 1, or if it was generally added in response to heisenbugs. My money is on the latter, but perhaps you're generally exposed to better codebases than I am.
Admittedly, I don't hang in the "I copied this code from Stack Exchange" or "IDE code completion is essential" crowds. When programming in C I always have the POSIX and C standards handy, even though I've memorized much of it already. I try to do the same for other languages, to the extent it's possible. Someone else linked to an earlier comment of theirs where they complained that the POSIX spec for fork was only the 8th hit on Google. Perhaps their problem was having a habit of Googling the usage for a core Unix API rather than using the freely available and eminently readable specs, which can be kept locally. I have well over USD $1000 of ISO specifications on my hard drive, and as far as specifications go the POSIX and C specifications stand out for their concision and clarity. This is true even when pitting them against the documentation for languages like Perl, Python, Go, or Rust.
Agreed, didn't mean to imply otherwise.
> ...
Agreed
> Perhaps their problem was having a habit of Googling the usage for a core Unix API rather than using the freely available and eminently readable specs, which can be kept locally.
To the degree that it is their problem, it's an understandable one. Not everyone has $1k to drop on text documents, and those standards may not cover the bulk - or even a particularly significant fraction - of the APIs one is dealing with on a day to day basis, making their value as an investment rather limited.
And two decades ago, I was gobsmacked to learn that C++ had a standard library at all, with it's own string type - std::string - that I could use where Borland's AnsiString was unavailable. Things have admittedly improved slightly since then, but you can't buy a spec without knowing it even exists!
And I'll admit I frequently google instead of refering to the standards I have bought. They're useful for resolving language lawyer disputes (although the occasional defect report limits even the standard's usefulness for that), but I won't call them particularly amazing references. Their main advantage is in being authoratative. Of course, implementations may not always perfectly match the standard they're supposed to match as well... when I care about the details, I'm more likely to dive into the implementation's source code rather than the spec.
And those problems have been this way for decades. The world you wish for doesn't exist. Not saying it shouldn't.
For someone who codes for a living and would be inclined to not check `fork` for an error code, would you mind sharing a bit about why you use that approach?
Usually when I write code, I assume it's my job to ensure the code works as designed in all reasonable circumstances. That requires understanding the failure modes of any external code I'm calling, at least as far as if/how it could fail.
IIUC, your approach is more like what I'd call prototyping. I.e., you accept that the code might be wrong, but you prefer to deal with that if/when it comes up in testing, so that you can move faster up-front.
Is that how you reason about it?
My suspicion (and naive hope) is that most people would not ignore that case if they knew it was dangerous. But I would expect them to ignore it if they didn't know it existed. Hence this article, I guess, which serves to remind everyone that it can in fact happen and is quite dangerous (due to a potential later interaction with kill).
I don't - the default behavior of simply ignoring I/O failures if, say, stdout's pipe was broken is usually what I want. In fact, I've had bugs in exception throwing languages where such stdout write failures threw, and I failed to explicitly catch and ignore them.
I also may skip error checking malloc. A null pointer exception / sigsegv / access violation "must" be fine if malloc is failing - even if I handle it nonfatally in our code, some of our closed source middleware doesn't, and neither do some system libraries. At best I can make slightly better fatal error messages for a subset of the resulting failures. If I'm trying to build super reliable software, I need to avoid exhausting/fragmenting memory badly enough for malloc to fail in the first place.
I have a decent chance of checking fork() for failure as I'm on the more paranoid end of error checking. I've seen enough weird junk like SetCurrentDirectory on a real directory failing due to NTFS filesystem corruption leading to an infinite loop - that I assume all documented error conditions will eventually occur somehow, as well as some undocumented ones.
But I've never seen it fail, and I'm probably just going to make it a fatal error.
I rarely use `malloc`, but I typically do check its return code. That's because in my experience, hunting down the root cause of segfaults can be a hassle, so I prefer to write code that will fail fast on failed memory allocations.
> That's because in my experience, hunting down the root cause of segfaults can be a hassle, so I prefer to write code that will fail fast on failed memory allocations.
This is a great reason to add error checking.
I've spent enough time hunting down harder dangling pointers and memory corruption bugs that alloc-returned-nullptr segfaults are trivial/second nature for me to figure out 99% of the time.
There is that remaining 1% of cases, for the already rare scenario where malloc has failed. Quite niche... but sometimes motivation enough for me to go through with adding error checking anyways. Especially if I actually encounter it.
If a failure would lead to data corruption, then yes. That this is important is made clear with gcc+glibc's _FORTIFY_SOURCE macro, which (e.g.) forces you to check the return value of sprintf() among others.
Of course, it's hard to imagine a case where printf() is part of a data pipeline that could be corrupted. So, nominally the answer is a simple `no'.
But it's a bad question, since fork() and printf() failures have different consequences. Also, boilerplate code for printf() never shows using the return value, whereas boilerplate for fork() always shows that. Your malloc() example is much better.
If malloc() failure leads to corruption, you'd better check the return value. Otherwise, sure, ignore it and let your prog just die is fine! Until it isn't ...
It's worth noting that "printf" specifically does not appear to be affected by _FORTIFY_SOURCE if the documentation I'm reading is to be believed.
> But it's a bad question, since fork() and printf() failures have different consequences.
Pointing out there are differences - via visceral example that likely hits close to home in your own practice as a programmer - is the entire point of the question, so I'll call it a good one ;)
The implicit point I'm making is that it's more complicated than "all syscalls must have error handling!" - which raises the question of, well, what does drive the choice anyways?. Ideally, perhaps a carefully reasoned consideration of the differing consequences you've pointed out. Or perhaps more likely, an adherence to examples and inertia. Or perhaps even more likely, a gut call based on things like "can I even think of when this might fail?", which one might answer "no" to - for having not yet developed a sufficiently mischevious imagination.
Regardless - having established the possible existence of nuance - this then lets us properly examine if our documentation does indeed properly prepare us to make these decisions, and how useful posts like the article might be for filling in where our documentation may have failed us.
> Also, boilerplate code for printf() never shows using the return value, whereas boilerplate for fork() always shows that.
Boilerplate for fork() does not always show that. As a concrete counterexample, the first google hit for "fork example" lacks error handling in any of their examples, although they at least mention the possibility of negative return values. I'll avoid linking it in an attempt to avoid improving it's undeservedly high search ranking, but if you're performing the search yourself, it's on the "geeks for geeks" website.
Maybe most sources of proper official documentation get it right these days, but I'm hesitant to assume even that.
> Your malloc() example is much better.
malloc vs fork failures also have different consequences. malloc failing is pretty unlikely to result in killing other processes (like your watchdog process!) for example. So perhaps it's just as bad of an example ;)
Raising awareness about bad APIs will also help people realize when they're about to create one themselves, which might cause them to switch designs.
To me, it seems better that there are fewer layers so that the architecture is clear, the elements of the OS and language are known, and the points where such errors are possible are clearly marked - as was done in this article submission.