Seems my last post met with some objections – somewhat rightfully so, as I mischaracterized one of Tom’s points – he never advocated just not checking for allocations, but instead to use an allocator that has a non-returning error handler – though it seems some of his commentors were advocating that (I think they should go to rehab). This isn’t always a bad way to go – I recommend using SafeInt the same way in some conditions. There are some problems that make allocators special, though. For example, say I write a DLL that allocates internally. If the DLL just calls exit() when it can’t allocate, this could cause some really bad experiences for users. Same thing if the DLL just tosses some exception that the client isn’t expecting. The right thing for such code to do is to either trap the error, and propagate it back up the stack, or be exception-safe code, and throw an exception that is caught just prior to returning to the client code, and throw an error there.
But wait – what about client code? I’ve said that if you get some weird input that results in bad math going into an allocator, then it _might_ be better to crash than to run arbitrary shell code, but the user experience still stinks. You’ve just improved things from disastrous to merely bad. Let’s look at a concrete example – PowerPoint is all really nice exception-safe C++ code, and they’re hard core about doing it right. Inside PowerPoint, allocators throw, and they handle errors where they make sense – typically by unwinding all the way back to where you can cleanly re-start the app, and not blow away other presentations you might be working on. PowerPoint, along with most of the rest of Office calls into mso.dll, which is mostly very much not exception safe. If it started throwing exceptions into say Excel, this would not be a good thing. Thus, that code has to check every allocation, properly clean up, and return an error.
The real kicker to all of this is that we’re entering a really interesting 64-bit world. My current motherboard handles 8GB, and if past trends hold, in the next 10 years, I could have over 100GB RAM in my system. The software we write today could well be in use for 10 years. Some whiz-bang 3-d video-intensive app could very easily think that allocating 6GB was a perfectly fine thing to do, and if crashes on one system and runs on another, that’s not a happy user experience. In the 64-bit world, we could have an allocation size that’s absolutely correctly calculated, doesn’t represent an attack, and it could quite easily fail on one system and not another. This isn’t the sort of thing we want to crash over. I’ve got an implementation of vi written around 1993 that just exits if the file is bigger than about 1MB – seems really silly now. I do have a game installed at home that had a bug in it because some library got upset when it tried to use > 2GB for a video buffer. Stuff like that is going to be annoying when I build my next system in a few years and have 32GB or so.
Another issue is that crashes are really serious exploits to server code, even if the service restarts – the perf implications are horrible. I’ve seen instances where someone wanted to run what was considered client-side code on a server, and trying to get it to server level of quality was tough. You usually don’t know when this might happen, so I’m kind of hard core about it – write solid code, and pick your poison – either don’t use exceptions and check ALL your errors, or if you have the luxury of dealing with exception-safe C++ code, then do the work to handle exceptions in the right places, and use throwing new. Note that server code has been moving to 64-bit for some time.
Yet another reason to check these things, at least in existing code, is that we do often correctly handle out of memory conditions locally – “No, you cannot paste a 2GB image into this document, try something smaller” being one example. If I then add a check for int overflows in the same function, I don’t want to in general introduce new error paths. What I can often do is make the int overflow show up as a bad alloc. For example:
size_t AllocSize(size_t elements)
if( elements > SIZET_MAX/sizeof(T) )
return ~(size_t)0; // can’t ever be allocated
return elements * sizeof(T);
It then becomes really easy to guard against int overflows in that code base.
Something else that jogged my memory – I wrote here about a great presentation by Neel Mehta, John McDonald and Mark Dowd on finding exploits in C++ code. The thing is that there aren’t many developer mistakes that don’t lead to exploits one way or another. This is actually independent of language – there’s things peculiar to C++ that can result in exploits, stuff that only C# can do, weird tricks with perl, and on and on. If you want to be a better developer, reading the Effective C++ series is a big help. If you want to be a better code auditor/tester, learn to be a better developer, and you’ll be better at spotting programming flaws.