Each of the earlier chapters focused on one particular topic. Each of those topics is vast and clearly warrants a dedicated chapter. In addition, several smaller topics (while no less important) don't quite warrant a chapter all their own. This chapter is a collection of those smaller topics.
Many security vulnerabilities are possible as a consequence of a programmer's omitting proper error handling. Developers find it extremely taxing to have to check error conditions continually. The unfortunate result is that these conditions often go forgotten.
If you have the luxury of designing an API, design it in such a way that it minimizes the amount of error handling that is required, if at all possible. In addition, try to design APIs so that failures are not potentially critical if they go unhandled.
Otherwise, appropriate exception handling can help you ensure that no errors that go unhandled will propagate dangerous error conditions. Use wrappers to convert functions that may fail with a traditional error code, so that they instead use exception handling.
There are plenty of situations in which assuming that a function returns successfully leads to a security vulnerability. One simple example is the case of using a secure random number generator to fill a buffer with random bytes. If the return value indicates failure, it's likely that no randomness was put into the buffer. If the programmer does not check the return code, predictable data will be used.
In general, those functions that are not directly security-critical when their return value goes unchecked are often indirect security problems. (This can often happen with memory allocation functions, for example.) At the very least, such problems are often denial of service risks when they lead to a crash.
One solution to this problem is to ensure that you always check return values from functions. That approach works in theory, but it is very burdensome on the programmer and also hard to validate.
A more practical answer is to use exception handling. Using exception handling, any error conditions that the programmer does not explicitly handle will cause the program to terminate (which is generally a good idea, unless the premature termination somehow causes an insecure state).
The problem with exception handling is that it does not solve the denial of service problem. If a developer forgets to handle a particular exception, the program will generally still terminate. Of course, the entire program can be wrapped by an exception handler that restarts the program or performs a similar action.
In C++, exception handling is built into the language and should be familiar to many programmers. We will illustrate via example:
try { somefunc( ); } catch (MyException &e) { // Recover from error type MyException. } catch (int e) { // Recover if we got an integer exception code. }
The try
block designates code we would like to
execute that may throw an exception. It also says that if the code
does throw an exception, the following catch
blocks may be able to handle the exception.
If an exception is not handled by one of the specified
catch
blocks, there may be some calling code that
catches the exception. If no code wants to catch the exception, the
program will abort.
In C++, the catch
block used is selected based on
the static type of the exception thrown. Generally, if the exception
is not a primitive type, we use the &
to
indicate that the exception value should be passed to the handler by
reference instead of being copied.
To raise an exception, we use the
throw
keyword:
throw 12; // Throw an integer as an error. You can throw arbitrary objects in C++.
Exception handling essentially acts as an alternate return mechanism, designed particularly for conditions that signify an abnormal state.
You can also perform exception handling in C using macros. The safe string-handling library from Recipe 3.4 includes an exception-handling library named XXL. This exception-handling library is also available separately at http://www.zork.org/xxl/.
The
XXL library only allows you to throw
integer exception codes. However, when throwing an exception, you may
also pass arbitrary data in a void
pointer. The
XXL syntax attempts to look as much like C++ as possible, but is
necessarily different because of the limitations of C. Here is an
example:
#include "xxl.h" /* Get definitions for exception handling. */ void sample(void) { TRY { somefunc( ); } CATCH(1) { /* Handle exception code 1. */ } CATCH(2) { /* Handle exception code 2. */ } EXCEPT { /* Handle all other exceptions... if you don't do this, they get propogated up to previous callers. */ } FINALLY { /* This code always gets called after an exception handler, even if no * exception gets thrown, or you raise a new exception. Additionally, if no * handler catches the error, this code runs before the exception gets * propogated. */ } END_TRY;
There are a number of significant differences between XXL and C++ exception handling:
In XXL you can only catch a compile-time constant integer, whereas in
C++ you can catch based on a data type. That is,
catch(2)
is invalid in C++. There, you would catch
on the entire integer data type, as with catch(int
x)
. You can think of CATCH( )
in XXL as
a sort of case
statement in a big
switch
block.
XXL has the EXCEPT
keyword, which catches any
exception not explicitly caught by a catch block. The
EXCEPT
block must follow all CATCH(
)
blocks. XXL's EXCEPT
keyword is equivalent to CATCH(...)
in C++.
XXL has a FINALLY
block, which, if used, must
follow any exception-handling blocks. The code in one of these blocks
always runs, whether or not the exception gets caught. The only ways
to avoid running such a block are to do one of the following:
Return from the current function.
Use goto
to jump to a label outside the
exception-handling block.
Break the abstraction that the XXL macros provide.
All of these techniques are bad form. Circumventing the XXL exception
structure will cause its exception handler stack to enter an
inconsistent state, resulting in unexpected and often catastrophic
behavior. You should never return from within a
TRY
, CATCH
,
EXCEPT
, or FINALLY
block, nor
should you ever use any other method, such as goto
or longjmp
, to jump between blocks or outside of
them.
XXL requires you to use END_TRY
. This is necessary
because of the way XXL is implemented as preprocess macros; true
exception handling requires handling at the language level, which is
a luxury that we do not have with C.
The syntax for actually raising an exception differs. XXL has a
THROW()
macro that takes two parameters. The first
is the exception code, and the second is a void *
,
representing arbitrary data that you might want to pass from the site
of the raise to the exception handler. It is acceptable to pass in a
NULL
value as the second parameter if you have no
need for it.
If you want to get the extra information (the void
*
) passed to the THROW( )
macro from
within an exception handler (specifically, a
CATCH()
, EXCEPT
, or
FINALLY
block), you can do so by calling
EXCEPTION_INFO( )
.
In some cases, the XXL macro set may conflict with symbols in your
code. If that is the case, each also works if you prepend
XXL_
to the macro. In addition, you can turn off
the basic macros by defining XXL_ENFORCE_PREFIX
when compiling.
Once you have an exception-handling mechanism in place, we recommend that you avoid calling functions that can return an error when they fail.
For example, consider the malloc( )
function,
which can return NULL
and set
errno
to ENOMEM
when it fails
(which only happens when not enough memory is available to complete
the request). If you think you will simply want to bail whenever the
process is out of memory, you could use the following wrapper:
#include <stdlib.h> void *my_malloc(size_t sz) { void *res = malloc(sz); if (!res) { /* We could, instead, call an out of memory handler. */ fprintf(stderr, "Critical: out of memory! Aborting.\n"); abort( ); } return res; }
If you prefer to give programmers the chance to handle the problem,
you could throw an exception. In such a case, we recommend using the
standard errno
values as exception codes and using
positive integers above 256 for application-specific exceptions.
#include <stdlib.h> #include <errno.h> #include <xxl.h> #define EXCEPTION_OUT_OF_MEMORY (ENOMEM) void *my_malloc(size_t sz) { void *res = malloc(sz); /* We pass the amount of memory requested as extra data. */ if (!res) RAISE(EXCEPTION_OUT_OF_MEMORY, (void *)sz); return res; }
XXL exception handling library for C: http://www.zork.org/xxl/