C Programming Guidelines
github.com
github.com
First, Zed Shaw's Learn C the Hard Way is not worth reading. I agree with most of Tim Hentenaar's criticisms.[2] It's confusing, incomplete, and overly opinionated. If you want to get started with C, read the K&R book. It's not perfect, but it's better than anything else I know of.
As for the guide itself, the quality of its advice varies greatly. Some bits are great. Some are good. Some are controversial. Some are dangerously double-edged. And some are Not Even Wrong. It would take too long to go into detail, so I'll just say: Please don't blindly follow this guide. Explore other codebases (including the ones the author linked to). Talk to other programmers. Write your own projects, and get feedback from those who are more knowledgable.
You might notice that this advice generalizes to every language. That's because C isn't special. If anything, the language itself is simpler than most. C has just had more time to accumulate cruft, both technical and cultural. So don't feel intimidated. Once you learn those bits of historical trivia, you'll be fine.
I'll take a look at the Silver Searcher's code and see what pearls of wisdom I can gain.
Honestly, I don't think ag is very good code. It's worth reading if you want to see optimizations like memory-mapped I/O, pthreads, Boyer-Moore strstr, etc. But if I had to grade the quality of the code, I'd give it a C-. Many parts have grown organically, and I haven't had time to restructure them. Also, ag's use-case is not exactly mission-critical. Nobody is going to die if it crashes or errors-out. Add my time constraints and… the result is some shoddy code.
I'll add "C Programming Guidelines" to my queue of blog posts to write, but I make no guarantees about when I'll get to it. Sorry.
http://www.amazon.com/Expert-Programming-Peter-van-Linden/dp...
One of my favorite tricks is to make your success case and your error cases look largely the same.
Say you have N allocations and N things that can fail. Lots of people get into trouble when they do:
void *p, *q, *r, *s;
p = a();
if (!p) { return ERROR; }
q = b();
if (!q) { free(p); return ERROR; }
r = c();
if (!r) { free(p); free(q); return ERROR; }
// etc..
And this madness just goes on and on. Add a new allocation? You potentially have to change n cleanup blocks.I breathe a lot easier when I can do [note we abuse the fact that free(NULL) is a no-op]:
void *p = NULL;
void *q = NULL;
void *r = NULL;
p = a();
if (!p) goto cleanup;
q = b();
if (!q) goto cleanup;
r = c();
if (!r) goto cleanup;
cleanup:
free(p);
free(q);
free(r);
What if the buffer at "r" needs to return something to the caller? It turns out it's really easy to transfer ownership in this pattern: // Success case. Give `r` to the caller.
*callerPointer = r;
r = NULL;
// Then we fall through to the failure block...
// r is null now so free(r) is harmless.
cleanup:
free(p);
free(q);
free(r);I think the C standard library would be easier to work with if they would've taken this further, and required, for example, fclose(NULL) to be a no-op.
There's always the counterargument that cleanup code that hides a missing allocation can mask bugs elsewhere, if for example a FILE * debug was never fopen()ed, only gets written to in exceptional cases, and fclose(NULL) ignores.
NOTES
The fclose() function does not handle NULL arguments; they will result in
a segmentation violation. This is intentional - it makes it easier to make
sure programs written under FreeBSD are bug free. This behaviour is an
implementation detail, and programs should not rely upon it.
I thought I remembered similar wording for glibc, but I appear to have been mistaken.But I do make a point whenever I'm writing C, if I write a cleanup function for a new type of struct or whatever, cleaning up null is always a no-op. It just makes life slightly less tedious. Having to remember which functions do this and which don't, I'll admit, is annoying.
Depends on the detail of the use case of course, but assuming that NULL cleanup is more the exception than the rule this is definitely a case of profile it first for me.
if (p != NULL)
checks across your codebase will have a higher I$ footprint than sharing a single check inside free(), so the overall performance impact could easily be positive.http://www.openldap.org/devel/gitweb.cgi?p=openldap.git;a=co...
http://www.openldap.org/its/index.cgi/Software%20Bugs?id=778...
Although in your example, if the code really was doing only that much, I'd recognise the ||/&& "ladder" structure and write it like this instead:
if(!(p=a()) && (q=b()) && (r=c()))) {
free(p);
free(q);
free(r);
}I guess the other point I am trying to make is that success and failure can hit mostly the same code. Your snippet only checks for failure.
Hint: grep linux sources for "goto" and re-evaluate your life decisions. :P
This is bad advice. A source-level debugger only works in source line numbers; if you ever need to set a breakpoint to find out when an error condition occurs you need to be able to set it on the action statement "goto fail" and not be bothered by the breakpoint tripping every time the if() gets evaluated.
Always separate the conditional from the statement.
What? No. Use plain "char" for strings (which may be signed or unsigned, depending on the environment).
If you use "signed char" in an environment where plain "char" is unsigned then a comparison like:
string[i] == '\xff'
will be always false.In fact, when working in constrained memory environments, like embedded 8 bit applications a char will often be used to do math, and then it makes a big difference. This is because there is no byte type by default in C.
There is in C99 and newer: int8_t or uint8_t.
C99 is old enough to drive
I'm using that from now on.----
From a draft version of the C standard:
The typedef name intN_t designates a signed integer type with width N , no padding bits, and a two’s complement representation. Thus, int8_t denotes such a signed integer type with a width of exactly 8 bits.
The typedef name uintN_t designates an unsigned integer type with width N and no padding bits. Thus, uint24_t denotes such an unsigned integer type with a width of exactly 24 bits.
These types are optional. However, if an implementation provides integer types with widths of 8, 16, 32, or 64 bits, no padding bits, and (for the signed types) that have a two’s complement representation, it shall define the corresponding typedef names.
If we're talking about C, we should use the technical definition of "byte" as used by C.
Everywhere else, sure, let's view byte as a 8 bit entity. That's so predominant that everything else would be nit-picking.
But not in the context of the C language.
(From 6.2.6.1: Values stored in non-bit-field objects of any other object type consist of n × CHAR_BIT bits, where n is the size of an object of that type, in bytes.)
qmail has the property that (if it is indeed secure, which is a disputed claim) it's secure by virtue of djb's superhuman awesomeness. I'm happy to believe that djb can come up with secure elliptic curves, but I'm under no delusions that reading all his papers and coming up with my own elliptic curves is in any way a good idea. Similarly, even if I take the djb way of software writing to heart, I don't see a reason to believe that I could write software with qmail's level of robustness. I am just not a world-class expert the way that he is.
seL4, meanwhile, has a formal model of the C programming language in the proof assistant Isabelle/HOL, a formal model of what they want their C codebase to do (and not do), and a formal proof that, in fact, their C codebase has the properties they hope that it has. The C is handwritten, but it may as well not be; you can't do feature additions, significant refactoring, etc. to the C without needing to replicate those all in a language that is completely unlike C.
If these are the best arguments in favor of writing secure software in C, you don't really need any counterarguments....
And here we are with millions of dollars/euros/yans/... spent in all forms of analyzers, theorem provers, hardware extensions (MPX, cheri) to workaround language flaws.
Older C isn't better when it comes to security and avoiding bugs K&R C is much beloved, but that innocent era is gone We can't afford to fight each other, or C will really die
--
Older doesn't mean there wasn't error handling and comprehensive bug checking. Some of us have been writing defensive code for years. Age of "style" does not imply approach to security. OpenBSD has a great deal of C code and is security conscious.
Really, articles like these are surpurfulus to the fact that there are shitty coders out there in any language.
This can be misleading...
void foo(char *buf) {
...
free(buf);
buf = NULL;
}
void bar() {
char *b = malloc(123);
foo(b);
// b isn't NULL...
}
My advice is: always compile and run with AddressSanitizer. Take the time to fix errors/leaks as soon as possible.It's confusing if you're mallocing in one scope and freeing it in another one (foo is especially confusing, besides being a disaster ready to happen). But if you write bar() like this:
void bar() {
char *b = malloc(123);
foo(b);
free(b);
b = NULL;
}
it looks all right.Not that I'm advocating this kind of use. I have... mixed feelings about it. But I've seen it used and it doesn't have to be confusing.
Its primary merit is that it makes use-after-free cases immediately obvious, since they're going to trigger a segfault.
But there are better ways to achieve that.
> Its primary merit is that it makes use-after-free cases immediately obvious, since they're going to trigger a segfault.
But then it hides double free errors.
int count = 0;
void foo_destroy(struct foo **f) { free(*f); count--; *f = NULL; }
void bar() {
struct foo *f = malloc(sizeof(struct foo));
foo_destroy(&f); // ok
foo_destroy(&f); // no error, but count is now wrong
}> It's just a minimal example.
I know -- but it tends to be confusing if you free() something in another scope than the one where you're malloc()-ing it. That's why your example, even if minimal, seemed confusing.
The obvious exception is if you have some reference counting mechanism/GC mechanism in place -- but then the wise thing to do is to not manually touch pointers all over the code.
void foo(char **const bufptr) {
...
free(*bufptr); *bufptr = NULL;
}
You can dereference `bufptr` at the top of the function to make the code more readable. This is how (almost) every object freeing function works in my project."Single line if possible: if(rc < 0) return rc;"
if (rc < 0)
{
return rc;
}
always IMO. // ...
if (rc < 0)
return rc;
// ...
It's easy to confuse the indentation for a block and end up with something like the apple bug.But when you don't break the line there's no confusion:
// ...
if (rc < 0) return c;
// ... if (rc < 0) return rc;
goto fail;
is no less confusing to me than if (rc < 0)
return rc;
goto fail;
Both can easily be misunderstood when you're quickly scanning through a bunch of code. Screen space isn't at such a premium that you can't afford a couple extra lines in a conditional statement. if (rc < 0) return rc;
goto fail;
Why would you indent hat next line? Presumably because you were trying to add it to the existing if statement's non-existant block. But why would you do that if you don't see a block?The problem with
if (rc < 0)
return rc;
is that we see the indentation and infer that there must be a block, so it's tempting to try to add to the block.When you keep your entire conditional statement on one line, it looks, to someone scanning the code, like just any other line. It doesn't look like a block at all, in fact it looks more like a function call.
Mistakes will happen. Ideally wrong code will look wrong. All of the following examples do the same thing. If you know the code is supposed to call Foo() and Bar() if bVar is true, which of these examples make it obvious that something is wrong?
if (bVar) Foo();
Bar();
if (bVar)
Foo();
Bar();
if (bVar)
{
Foo();
}
Bar();
if (bVar)
{
Foo();
}
Bar();
The third, followed by the fourth, makes the mistake most obvious to me.For example, let's say there are 20 case where it's a single line return, then shouldn't be "if(rc < 0) return rc;" better. On the other hand, if the other condition in that block of code all need brackets, it just seems good sense for the single line condition to have bracket too.
It could be extending pretty much to all other syntactic sugar even in higher level language (map + lambda vs for loop?)
if(rc < 0) goto fail;
as a countermeasure for that breed of bug. if((rtn = func(/*whatever*/)) < 0)
{
// some hint why things may fail
goto barfo;
}(Edit: This is not to say that there can't be multiple good stylistic options, just more that the bad ones by far outnumber the good ones and at the end of the day it does matter how your code looks if you plan on supporting it for a long time)
I don't really get what they're after with this section. Can anyone shed some light?
struct thing athing;
struct thing *thingptr = &athing;
Then there should be no confusion as to whether you write: athing.member = 12;
or: thingptr->member = 12;This same logic applies to real time operating system vendors. Everything is written in C. The minute they start writing their code in a different language is the minute their competitors become that much more attractive. On top of that, one of the major selling points of the major RTOSes is that they have mature code bases with decades of bug fixes and products using them.
Perhaps this presents an opportunity for disruption, but it seems like a steep hill to climb.
Performance
The restrict keyword should be used rarely if ever
wut?What MDB is being referred to here?
sqlite and lmdb aren't exactly equivalent, but I guess that is his point; you can gain a lot of performance just by understanding your use case and what tradeoffs you can make.
If you're dealing with POSIX / the C stdlib, mirror it and use char* . (The string version of "Respect the integer types used by each API."; if you're coding for Windows you'll likely need to use LPWSTR or whatever they call it now.) But: if you're storing UTF-8 string data, you _want_ unsigned char (and maybe uint8_t[2]): if start trying to inspect the code units, esp. to decode them to code points, you're going to need to do comparisons and bit manipulations on them, and its too easy to write `code_unit < 0x80` or something similarly silly (e.g., while masking bits to see if it's a continuation byte). It's like the article early today called "Should I use Signed or Unsigned"[1] states: "Prefer unsigned when using bitwise operators".
> Limit recursion
Recursion is fine; you simply need to be aware of your worst case stack depth. You're looking, really, for the complexity of your stack usage, or the big-oh of your stack size. If you're doing something like recursing down an in-memory balanced binary tree, it's O(log(n)) in the worst case: ~64 stack frames on a 64-bit CPU. Can you fit that many on the stack? Certainly that's heavier than a normal function call, but it's still a finite, computable size.
If you can't compute that / prove an upper bound, then I agree.
> Aside: any language that uses keywords like public and private to determine visibility instead of placement/scoping is bad
If not with keywords, how should visibility be determined? (How does "placement/scoping" determine visibility?)
> Use get/init or create/copy/alloc in function names to indicate ownership versus borrowing for return values
Inevitable, I feel like you end up creating some structure, let's call it a Foo. So you then have a Foo * Foo_create(), a Foo_destroy(Foo * ). Some action performable on Foo? Foo_reticulate_splines(Foo* , …). Combined with,
> You should generally track used and unused space in dynamic arrays,
and the overhead of writing functionality to deal with dynamic arrays in C, the entire section about managing strings, and you've come to the very reason I left C: I'm already writing C++. Foo_create is a ctor; Foo_destroy a dtor; Foo_reticulate_splines a member function. Combined with vectors & strings taking care of day-to-day memory management, and I'm left with little reason to use C.[3] RAII adds some degree of object lifetime management that simply isn't present in C (but does not add the compile-time checks that I'm starting to love Rust for.)
[1]: http://blog.robertelder.org/signed-or-unsigned-part-2/
[2]: I know C++ got some decent types for Unicode _very_ recently; I'm not aware if C got those.
[3]: I am not suggesting C++ to be the language you _should_ be using. I simply cannot justify ever using C over it.
Back when I got to learn it, around 1993, I had already used Turbo Pascal 3, 5.5 and 6.0.
C seemed so primitive and unsafe in regards to Turbo Pascal features.
However the same person that got me into C, also showed me C++.
There it was, a language that could be used as safely as Turbo Pascal, provided one made use of the language features instead of the C copy-paste compatibility.
When Turbo Pascal stopped being a possibility, I joined ranks with the C++ side, in the C vs C++ flame wars.
Even teached C++ at the university as a senior student.
Only used C instead of C++, when asked to do so by university teachers or customers.
Edit: Here is a paper that touches a bit on what I'm describing: http://people.cs.umass.edu/~emery/pubs/berger-oopsla2002.pdf
Also, to clarify, I'm not saying allocate yourself a big piece of memory and then reimplement malloc on top of that. You literally just get a big piece of memory and every time you want to allocate something new from it add the offset to the base address, return that to the user, and store the new offset.
1) Reinventing malloc for no good reason 2) Opening yourself up to a whole world of security holes
See Bob Beck's talk on the first 30 days of LibreSSL. Heartbleed wouldn't have been nearly as catastrophic if OpenSSL just used the system memory allocator like normal people.
The semantics are simple as well:
region create_region(size_t size);
void * allocate(region *r, size_t size);
void destroy_region(region *r);
Edit: also, you're not reinventing malloc for no good reason. A region based allocator is much faster, possibly at the expense of using more space than the system allocator (because you might reserve more space than you actually need).If you're really worried that you'll leak an entire region worth of data just allocate the region with malloc instead of mmap and then use Valgrind to tell you what regions you aren't destroying.
Yes, if your program just needs to allocate lots of memory, do some computations, then exit, this approach works. But the programs where security is most critical do not usually follow that pattern.
Look, I'm totally willing to accept that there might be flaws with this approach, but with the exception of adrusi, no one's objections have been all that reasonable. If there are legitimate objections (not just handwavy, oh, you'll probably implement the allocator wrong) I'd love to hear them. I use this pattern in my own code and I'll stop if there are legitimate flaws.
Also, Akamai released a patch for the OpenSSL allocator bug mentioned earlier. Guess what the patch used: mmaped regions.[2]
[1]: http://www.gnu.org/software/libc/manual/html_node/Obstacks.h...
[2]: http://thread.gmane.org/gmane.comp.encryption.openssl.user/5...
It is entirely possible to have no bugs in the <100 lines needed for this code, so I don't think the bug argument is valid. adrusi did raise a legitimate concern however.
In other words, the malloc-versus-mmap distinction here isn't the important one, it's the custom allocator versus system allocator one.
True.
Many (most?) programs can mmap a large region of memory and allocate from that.
You don't even need to do that for many applications. Coming from an embedded systems background where dynamic allocation is highly discouraged and almost never necessary, the amount of superfluous allocation/deallocation I see in a lot of code is astounding; maybe it's because of my experience, but I can often very easily find a way to do something without dynamic allocation and consequently O(n) space, with what usually turns out to be simpler (thus less chance of being buggy) code and O(1) space.
But when you do need dynamic allocation, malloc() isn't bad at all.
The problem with doing custom allocation is not specifically due to bugs in the allocator per se, but because your custom allocator will probably not be hardened against other bugs like some other allocators are. Specifically you will piss off the OpenBSD people who have put a lot of work into their allocator with "exploit mitigations."
A bump allocator is pretty much guaranteed to let overflows (reads and writes) touch other valid memory. A hardened allocator will use guard pages and other tricks to prevent those bugs from being exploitable.
One thing you can do is use bump allocators within "security zones." For example data related to a single user can be allocated together without much risk, but data from separate users is kept separate.
This is very different from the world of game programming. I've been wondering lately if there will be some sort of bifurcation of the field.