Note: At the end of part 1, I’d suggested that part 2 would be about invalid handles. This post however isn’t about invalid handles. That’ll be the next part in this series.
I very frequently come across code like this:
void dosomething( _In_ somestruct* in ) {
//Not detected at compile time
assert(in->some_precondition);
}
The function, dosomething, expects/requires a specific condition. So, the author assert
s it, and resolves to test every possible code path, thus verifying code correctness. When all their tests pass, they build in release mode, and the assert disappears.
Some time later, someone calls dosomething under different conditions, all of their tests pass, and they release the modified code. Users start seeing their data mysteriously corrupt itself, and their once-reliable program is crashing in ways that nobody can reproduce. After several days worth of investigation, developers thoroughly audit the code, and spot the new (and incorrect) code path. They refactor the faulty code path, add a VERY LOUD comment to dosomething, and forget about the whole thing.
Some number of months later, the same thing happens. This time, a developer has a bright idea: Let’s return an error code! Whoever wrote the code without a proper check for invalid arguments is an idiot!
The function now looks like this:
HRESULT dosomething( _In_ somestruct* in ) {
//Not detected at compile time
assert(in->some_precondition);
if ( !in->some_precondition ) {
return E_INVALIDARG;
}
}
We’re better off now, or at least we think so.
Now of course, everybody has to check the return code, which means more complexity, more ways to go wrong, and more code paths to test. Since there are far too many code paths to test every single one, some end user might still get to one of the untested, and also invalid, path.
Yet a third developer comes along, and thinks: What a silly C programmer. Someone will inevitably ignore the return value and the error will go unnoticed. Even worse, because I have to check the return value every single time, I can’t compose this function, and thus it’s less practical. This is an exceptional case, so we should treat it as one.
The function now looks like this:
void dosomething( _In_ somestruct* in ) {
//Not detected at compile time
assert(in->some_precondition);
if ( !in->some_precondition ) {
throw std::invalid_argument( "Function preconditions not met!" );
}
}
We’re a little bit better off now, at least we’re pretty sure that we are.
Now, exception safety becomes an issue. We still have to test every possible code path to make sure that none of them incorrectly call dosomething. We still can’t be confident about it.
Perhaps somebody else comes along, and writes a wrapper object, thus only “allowing” correct uses of dosomething. Now everybody has to rewrite their code use that wrapper.
Complexity has increased, this function has been refactored thrice, and we still haven’t even found a way to be code-correctness-confident, at compile-time, for all the uses of dosomething.
The cycle repeats, no end in sight.
Nearly every program stops there – they choose error codes or C++ exceptions, to carefully handle bad logic WHEN it happens. Preventing them from happening, well that becomes an issue of “professionalism” and “discipline”.
Neither “professionalism” nor “discipline”, actually prevent the mistakes of logic, but blaming the mistake on a given individual’s moral or personal failings works nicely to…well I’m not actually sure; I have noticed it is a fairly pervasive and pathological mindset in case studies of engineering disasters (Therac-25, Toyota electronic throttle malfunctions, others). But, I digress.
A few examples from Microsoft’s headers
One of Microsoft’s motivating factors in developing SAL, was the infamous blue screen. Drivers. particularly kernel-mode drivers, are extremely sensitive to misuse. A failure in the kernel can mean system instability, serious security vulnerabilities, or massive data corruption. Furthermore, the kernel APIs that drivers rely on are often very complex, interacting with many components of hardware and software – and thus very hard to get right.
Continue reading ‘Preventing bugs, and improving code quality with Microsoft SAL (Part 2, custom preconditions for structs & objects)’
Posted in Computing, Featured Posts, Science/Innovation
Tags: Code Analysis, Formal Verification, Microsoft SAL, Source Annotation Language, Static analysis