12 ms·
A critique of "How to C in 2016"
- deleted 11y ago[deleted]
- unwind 11y agoAah, this felt like a much-needed piece. The original text was so long, it kind of made my eyes glaze over and I never found the time to do this level of detailed critique. Well done!
- eloy 11y agoNot to be confused with Ken Thompson.
- rocks 11y agoAlmost fell for that....
- avian 11y ago> Zeroing memory often means that buggy code will have consistent behavior; by definition it will not have correct behavior. And consistently incorrect behavior can be more difficult to track down. I agree that using calloc should not be an excuse for writing sloppy initialization code. But in my experience, inconsistently incorrect behaviour (e.g. heisenbugs that tend to appear and disappear depending on the content of some random chunk of uninitialized memory that happens to be in an unfortunate state after malloc) are one of the hardest type of bugs to fix. It's the type of the bug that occasionally happens in production and typically can't be reproduced in a controlled manner in a development environment. I certainly prefer consistently incorrect behavior.
- XorNot 11y agoIt's also more secure. We've just this week now had an SSH bug where random chunks of memory get dumped somewhere they shouldn't (not that I'm clear on if calloc would help in this case, but the issue remains). Zeroing memory is just good habit - it's means you don't wind up accidentally leaking things when you make a mistake. And you should assume you will.
- JoachimSchipper 11y agocalloc is not relevant to that issue - it's an out-of-bounds read. (Good ASLR does help, by turning such issues into crash bugs.)
- anon1385 11y agohttps://www.qualys.com/2016/01/14/cve-2016-0777-cve-2016-0778/openssh-cve-2016-0777-cve-2016-0778.txt https://www.qualys.com/2016/01/14/cve-2016-0777-cve-2016-077... >- "out_start == out_last" (lines 205-206): no data was ever written to out_buf (and both out_start and out_last are still equal to 0) because no data was ever sent to the server after roaming_reply() was called, but the client sends (leaks) the entire uninitialized out_buf to the server (line 214), as if out_buf_size bytes of data were available.
- TazeTSchnitzel 11y agoHeartbleed wasn't an out-of-bounds read.
- user9982 11y agoThis bug isn't heartbleed. Also Heartbleed was SSL, not SSH.
- TazeTSchnitzel 11y agoI'm aware.
- notalaser 11y agoIf OpenSSL had used calloc() everywhere instead of malloc(), Heartbleed would have still happened.
- dietrichepp 11y agoThe tools for rooting out these problems have gotten a lot better, thankfully. I'm very fond of the address sanitizer. Using calloc() everywhere can prevent the address sanitizer from finding problems.
- neopallium 11y agoGCC (>=4.8) & Clang now have Address Sanitizer support for fast memory error detection. About a 2x slowdown [1]. 0. https://github.com/google/sanitizers/wiki/AddressSanitizer https://github.com/google/sanitizers/wiki/AddressSanitizer 1. https://github.com/google/sanitizers/wiki/AddressSanitizerPerformanceNumbers https://github.com/google/sanitizers/wiki/AddressSanitizerPe...
- gsg 11y agoYou could try to split the difference by using a wrapper around malloc that zeroes in production and doesn't under testing. Of course, running significantly different code in production and test has its own issues.
- DrJokepu 11y agoI mean, reading zeroed out memory is not really incorrect behaviour anyway. If you know that it was zeroed out, then you're really just reading memory that was initialised with a specific default value. That being said, zeroing out everything you allocate will make it difficult for the kernel's paging system to defer allocating you a physical page until you actually need it, so you will need to be a lot more careful with your allocations if your application uses relatively large amounts of memory and you need to work with memory constraints.
- rc4algorithm 11y agoUnless the zeroing is defined by the language, assuming it t is incorrect and shouldn't be done.
- lmm 11y agoCalloc is defined to return memory that's all-bits-zero, which in turn defines the value for many datatypes. The overwhelming majority of C implementations additionally define the values of all-bits-zero floats and pointers. IMO it's reasonable to write programs targeting only such implementations, provided such a dependency is clearly documented (just as e.g. it's reasonable to write C programs targeting only implementations on which floating-point arithmetic is IEEE 754, despite this not being required by the C standard).
- rc4algorithm 11y ago> The overwhelming majority of C implementations additionally define the values of all-bits-zero floats and pointers. So... Clang and GCC? Regardless, you're ignoring the obvious difference: that isn't POSIX- or ISO-defined at all. If you choose to use it, you're using a weird dialect. Might as well just switch to Cyclone.
- lmm 11y ago> that isn't POSIX- or ISO-defined at all. If you choose to use it, you're using a weird dialect. People used C (even on multiple compilers) long before POSIX or ISO. You're putting the standardization cart before the compatibility horse.
- appleflaxen 11y agoGreat point. This is the part I disagree with most strongly: > And consistently incorrect behavior can be more difficult to track down.
- rc4algorithm 11y agoAll of that useless zeroing is very painful from a performance perspective. It just isn't a reasonable option in performance-relevant code like kernels and core infrastructure. The Moore's Law argument doesn't apply because memory bus latencies and case pressure are lasting problems.
- efaref 11y agoOn high-performance code for an embedded PPC system I used to work on, we made all our control block a multiple of the L1 cache width. Our allocation routines then all had inline assembler to run the dcbz instruction (data cache block zero) on all the cache blocks for the control block as it was allocated. This meant the control block was always zeroed, and the memory bus wasn't touched in order to do so. Yes, things were evicted from the cache, but since we're about to start writing things into the control block, the lack of fetch was a net gain.
- ori_b 11y agoA better solution in this case is to fill the memory with junk -- 0xdeadbeef is a popular solution.
- geocar 11y agoI'm divided. I think it is good to share our ideas and our theories with each other because the discourse can stretch our minds and help us better imagine the perspective of another. Dialogue can foster creativity and invention, and an honest argument can embiggen friendships like nothing else. On the other hand, opinions are like assholes.
- grabcocque 11y agoThis, ladies and gentlemen, is why C gives me the willies. And why this happens: http://www.cvedetails.com/vulnerability-list/year-2016/month-1/January.html http://www.cvedetails.com/vulnerability-list/year-2016/month...
- pjmlp 11y agoNot that it would eliminate security exploits caused by logical bugs, but most those exploits were already taken care of with Burroughs Algol variant (1961), Modula-2 (1978), Cedar (1981), Ada (1983), .... but oh well.
- benbenolson 11y agoThose are due to programmer error. The fact that C doesn't lie to you means that it takes a bit more experience to program properly; as with all things, with great power comes great responsibility. You don't blame the gun for killing people, you blame the person for shooting it.
- legulere 11y agoNot even the very best C programmers will write memory safe code. When you write C you will write exploitable code, no matter who you are. Don't blame the programmer for stepping onto a mine in the minefield, blame the minefield.
- whyever 11y ago> Those are due to programmer error. How does that help? Almost all security vulnerabilities are due to programmer error. > You don't blame the gun for killing people, you blame the person for shooting it. It's more like choosing between a gun that makes it really easy to shoot yourself in the foot by accident and one that is safer.
- legulere 11y agoI disagree with many of the arguments. Many of them are ignoring the reality of today. One example: > If you want bytes, use unsigned char. If you want octets, use uint8_t Bytes and octets are the same today. I never came across a system where this wasn't true. I was never told about a system where this wasn't true. I wasn't even told that there once were systems where this wasn't true until fairly recently.
- omginternets 11y agoWhat was the original distinction between bytes and octets? Byte = word vs octet = 8-bits, i.e. a byte is not necessarily 8-bits long?
- TazeTSchnitzel 11y agoA byte is the smallest addressable unit. An octet is a unit of 8 bits, which is a common size for a byte.
- caf 11y agoA byte is the smallest individually-addressable unit.
- EliRivers 11y agoOh, well, if you haven't seen something then it must not exist. "I wasn't even told that there once were systems where this wasn't true until fairly recently." Wait, so someone filled in a piece of your ignorance and your response is still "Bytes and octets are the same"?
- sipior 11y agoTo be fair, what they actually said was, "Bytes and octets are the same today". It's a little disingenuous, I think, to quote everything but the last word, especially since the qualifier "today" was kind of their entire point.
- 11y ago
- rplnt 11y agoWhat's up with creating "empty" repositories for articles? Isn't there a better solution? I think using gist would be more suitable if you want the article to be attached to your github username...
- stupidcar 11y agoIronic that he begins by taking issue with the idea that you should avoid writing C if you can, then proceeds to provide the best evidence possible for why it's true. Why on Earth would you voluntarily code in a language where people can debate something as simple as which type to use for integers, unless you absolutely had to?
- EliRivers 11y agoBecause it's simple and low-overhead and compilers for it exist on every lump of hardware I've ever had to work with, from a clunky x64 to a PIC to a TigerSHARC to a bizarre DSP thingy from a tiny fab lab somewhere. A great many of the problems that people experience with C code can actually be nullified (zing!) by finding a competent C programmer who does the job properly. A tool being easy to misuse doesn't mean we shouldn't use it; it just means we should demand high standards of the programmers and not employ some chancer who learned how to program from the back of a cereal packet. I give young children plastic safety scissors. I give competent adults chainsaws and scalpels.
- ketzu 11y agoI think what the discussion about this statement misses is the "unless you need to" part. There are cases where you should use C! It is not "never ever use C". If the use-case is being able to run on everything from TigerSHARC to x64, you probably should use C. If you want a GUI based Windows App or a webapplication, that might not be a "you need to use C" case.
- pjmlp 11y agoSee, that is the problem right there. Even if one does find such competent programmers, I doubt they the way C culture works it would help. Most C devs I have met, if given a memory safe systems programming language like Modula-2 or Rust, would just use SYSTEM or unsafe {} everywhere without any proof of a profiler if it really matters to the customer. As for being available everywhere, that is an historical accident of C's adoption. I remember when C was only available in UNIX and we had things like RatC.
- Mayzie 11y ago> But zeroing a structure with memset, though it will set any integer members to zero, is not guaranteed to set floating-piont members to 0.0 or pointers to NULL (though it will on most systems). Why is that? On those systems, is a sequence of 0 bits not considered 0.0f or NULL?
- masklinn 11y agoNot sure about fp, but the C standard very carefully and explicitly notes that NULL pointers doesn't mean a runtime value of 0. The comp.lang.c FAQ 5.17[0] further provides examples of nonzero NULL pointers e.g. > The CDC Cyber 180 Series has 48-bit pointers consisting of a ring, segment, and offset. Most users (in ring 11) have null pointers of 0xB00000000000. This is a bit confusing because the "null constant" is an integral 0 e.g. `(void ) 0` is a null pointer, but it's defined as an arbitrary compiler instruction. `(void ) foo` where foo is 0 at runtime may not be a null pointer at all. [0] http://c-faq.com/null/machexamp.html http://c-faq.com/null/machexamp.html
- Anilm3 11y agoOne of the most important reasons to use calloc, other than the zeroing of the allocated memory, is the fact that it checks for integer overflows (at least on most implementations). For example, when allocating an array of n elements, in malloc you would do something like the following: ptr = malloc(sizeof(int) * n); Which could potentially lead to an overflow of the size passed to malloc, hence allocating a significantly smaller buffer and end up accessing adjacent memory which hasn't been allocated to the buffer (buffer overflow) Calloc requires two arguments, one for size and one for number of elements, which allows it to check for an overflow before performing an allocation (e.g. by using SIZE_MAX): ptr = calloc(n, sizeof(int)); This is not to say that zeroing memory is not useful. There are many situations in which a zero in a memory block represents the end of the usable data, as for example in a string, so zeroing memory in those situations is definitely recommended.
- xcgvgh 11y agoUsing calloc instead of malloc, just for checking arithmetic overflow, is superfluous. You can always write a check or a wrapper for malloc that does that automatically. It is so easy I can write it here: _Bool Check( const size_t a , const size_t b ) { if( a > SIZE_MAX / b ) { return false; } return true; } (If proper warnings are enabled, it also provides extra integer type checking.)
- Anilm3 11y agoAgreed, but there's little point in reinventing the wheel in this particular case.
- xcgvgh 11y agoThere is no reinventing going on here. In C you have to write relatively low level code. This includes manually calling allocs for objects, which includes the code for checking your arithmetic. At some point you will have to write that code. If you are smart you will wrap it into a function.
- xcgvgh 11y ago>size_t is defined as "an integer capable of holding the >largest array index" >>No, it isn't. Could anyone elaborate? I don't see how could someone allocate more than SIZE_MAX bytes with (m/c/re)alloc since they all take size_t as an argument, and size_t is the type used for defining array sizes. Thus size_t can hold the largest array index (in fact it can hold the largest_index+1 since arrays are zero based). Any counterarguments?
- chrisseaton 11y agoMaybe he's taking it very literally, as I think size_t is literally defined as 'the integer type of the result of the sizeof operator'. So it literally isn't defined as 'an integer capable of holding the >largest array index' as that's not the definition and words they use.
- viraptor 11y agoBut if sizeof returns size_t, then your can't have indexes larger than that. Otherwise you could point beyond the largest in-memory object possible. (Using smallest (byte) indexing) But yes, he may be arguing the definition rather than meaning.
- dietrichepp 11y agoThe counterargument is actually relatively simple, because it's an argument over what the definition of size_t is, and you can simply go look to the C standard and find the definition there.
- xcgvgh 11y agoI understand. If I change the argument to: size_t is capable of holding the largest array index", then this must be correct, or is there still something I missed?
- dietrichepp 11y agoYes, that's correct.
- cesarb 11y ago> Converting to uintprt_t will give you some result -- but that result won't be useful. Unless you're writing low-level system code, don't do any pointer comparison or arithmetic unless both pointers point into or just past the end of the same object. I disagree. There are situations where you need an ordering between unrelated objects, but the precise order doesn't matter, only that it's consistent. An example is when you have a complex data structure with one lock per node, and you need to lock more than one node at a time for some algorithm. To prevent a deadlock, you must always take the locks in the same order. Comparing the address of the locks gives you a total order over all the locks, which is enough.
- zodiac 11y agoI think comparing (using <) pointers to unrelated objects is undefined behavior. So you do not get a consistent ordering.
- cesarb 11y agoThe part I quoted is talking about casting the pointers to uintptr_t (which is defined behavior) and comparing the resulting uintptr_t (since it's an integer, comparing it is defined behavior). It's not talking about comparing the pointers directly (which is undefined behavior). His argument is that, while defined behavior, "(uintptr_t)ptr_a < (uintptr_t)ptr_b" is useless; I disagree, since it's a well-known trick to prevent AB/BA type deadlocks.
- coldtea 11y agoI find myself to disagreement with almost ALL the points this "critique" makes. They are either minor pedantic corrections, "it's how it's always been done" things, or "you might have your reasons for doing it in a bizarro way" affairs. Sorry, but for 90% of use cases, the original article has better advice.
- Manishearth 11y agoAgreed. Most of the integer stuff is pedantry ("doesn't make it non-standard" -- who cares, the point being made was that these types are better). There are some valid and useful points being made, but most would serve better as caveats on the original article rather than straight out "don't listen to this".
- lmm 11y ago> int in particular is going to be the most "natural" integer type for the current platform. If you want signed integers that are reasonably fast and are at least 16 bits, there's nothing wrong with using int The slight performance gain on a few systems that you get from using int rather than int16_t is very rarely worth the hugely increased risk of introducing platform-specific undefined behaviour, IMO. > float and double are very commonly IEEE 32-bit and 64-bit floating-point types, particularly on modern systems, but there's no guarantee of that in the language. True. Is there an alternative? Something like float32_t? > But more often all you need is a particular range of values. For that, you can use either the [u]int_leastN_t or [u]int_leastN_t types, or one of the predefined types. Sure - but again, how much performance do you gain from using a _leastN type, and how much reliability do you lose? > It's capable of holding the size of the largest object your implementation supports. (There's an argument that that's not necessarily guaranteed, but for practical purposes you can rely on it.) It can hold the largest memory offset if all offsets are within a single object. But you just said in the previous section that it only makes sense to perform pointer arithmetic within a single object anyway. > There is a widespread convention, particularly in Unix-like systems, for functions to return 0 for success and some non-zero value (often -1) for failure. In many cases different non-zero results denote different kinds of failure. It's important to follow this convention when adding new functions to such an interface. (0 is used for success because typicallyi there's only one way for a function to succeed, and multiple ways for it to fail.) The convention is not as widely accepted as this section implies. When adding to an existing system one should follow that system's conventions. When writing for Unix one should follow the Unix conventions. But there are other conventions (e.g. the VMS one) that also see use in C. > I don't often use automatic formatting tools myself. Perhaps I should. Yes, you should. > Zeroing memory often means that buggy code will have consistent behavior; by definition it will not have correct behavior. And consistently incorrect behavior can be more difficult to track down. Disagree strongly. Consistently incorrect behaviour is much easier to track down than inconsistently incorrect behaviour. > if you're trying to program defensively, you might consider initializing allocated memory to some value that's known to be invalid rather than one that might be valid. Agreed - but how? One of the biggest problems with C is that it is extremely hard to mark a state as invalid (hence the whole integers for error codes discussion earlier, where really an error code should have a different type from a valid integer result).
- cataphract 11y agoIsn't another problem of "ptrdiff_t diff = (uintptr_t)ptrOld - (uintptr_t)ptrNew;" that you're assigning an unsigned value to a signed variable? If ptrNew is larger than ptrOld this can give a very large value via wrapping around, so large that it will probably be bigger than the max value of ptrdiff_t, making the assignment a form of signed overflow.
- xcgvgh 11y agoThe assignment is a conversion from unsigned to signed, if the value cannot be represented by the signed type, the result is implementation defined or an implementation defined signal is triggered.
- cataphract 11y agoRight. Implementation defined, not undefined. Still not great for portable code, which is what the article is all about.
- xcgvgh 11y agoIt's also very dangerous.
- appleflaxen 11y ago> The first rule of C is don't write C if you can avoid it. My C skills are non-existent, but it occurs to me while reading the response that when two people who have so carefully considered the subject can arrive at such different places... then yes, you probably shouldn't write C if you can avoid it.
- benbenolson 11y agoC is the only language that will run on certain strange systems, and that is the only reason that most of these quibbles even matter. It's more of an argument of "What is the most portable way to write C?" C is a very small and elegant language that never lies to you. There aren't any useless functions that are specific to one use case, and things that you don't really, really need are simply left out of the language. This means that instead of spending hours learning the language and all of its quirks, you get to focus on the problem that you're trying to solve, and come up with the most efficient solution possible (there's a reason why almost all performance tests use C as 1). You don't have any language quirks like those that are discussed in this article, unless you are running an architecture that has similar quirks, in which case you're going to have quirks no matter what you do. Edge use cases, as with everything, may cause a bit of quirkiness. That being said, don't be scared off from C just because of these pedantic arguments.
- appleflaxen 11y agoThat's a great point; thanks for the context.
- pjmlp 11y ago> C is a very small and elegant language that never lies to you. I would call undefined behavior and compiler optimizations lies.
- GregBuchholz 11y ago>C is a very small and elegant language that never lies to you. I've liked: http://www.gowrikumar.com/c/index.php http://www.gowrikumar.com/c/index.php ..and... https://www.google.com/search?q=expert+c+programming+deep+c+secrets https://www.google.com/search?q=expert+c+programming+deep+c+...
- fithisux 11y agoThe discussion usually boils down to a. We need a better C b. We need better programmers. Personally I believe C tries to hard to be high level instead of what it really is. Newer revisions should try to make it lower level. For example I never could understand why we need enums. Enums are very useful for Java or Golang which are high-level. C never needs enums. Why should I ever put const in the argument of a function? It makes sense for D but for C no. Why should I ever use complex in C. Is there any HW that supports complex numbers? If it becomes standardized, then why not? C should be good to write kernels and simple utilities and simple compilers. For the rest, use a high-level language.
- pjmlp 11y agoThe thing is, such higher level systems programming languages were already a thing in the 60's. C designers just wanted to hack something for their toy OS and here we are.
- woodman 11y ago> For example I never could understand why we need enums. > Why should I ever put const in the argument of a function? Reducing potential state and conveying programmer intent. Both lead to safer and more performant code (by way of compiler optimizations).
- fithisux 11y agoWhat you say is incompatible with the "Cross-platform assemble", simple language ideals. If that was the intent why not throw namespaces and other goodies in?
- woodman 11y agoWell those ideals are pretty subjective, I'd say that the ideal language would be flexible, performant, safe and productive. As those goals conflict with one another, the ideal language would find the perfect balance. I don't know if C has found that balance, but it has had a pretty good run. Defining sets (enums) and memory access constraints (consts). I wouldn't describe that as high level from a conceptual or practical perspective. Namespaces are simple conceptually but not in practice. The same can be said about the beautiful logical consistency of S-expressions.
- oldmanjay 11y agoReading this as someone who neither loves not loathes C, I would not have written this as a series of rebuttals. It makes the essay seem defensive and nit picky. I get more of a sense of emotional attachment to a particular way than any sort of reasoning.