Get on-the-go access to the latest insights featured on our Trustworthy Computing blogs.
Hello all - Dave here...
Tim Burrell of the TwC Security Science team presents the fifth blog installment describing /sdl: functionality in Visual Studio 11.
In the last few posts we described some additional security features that are enabled by the new /sdl compiler switch. Previous comments to posts in this series have highlighted the balance that we have to strike when introducing new security features: some developers/readers welcome new security features in the toolchain and suggest further enhancements; for others conformance and standards compliance takes precedence.
This post describes another feature included under /sdl: in limited circumstances the compiler will instrument C++ operator::delete calls to sanitize the pointer reference. In particular it illustrates a case where we have sought to carefully balance the desire for improved security with that of conformance and avoiding unnecessarily breaking a developer’s valid C++ code.
The Security Development Lifecycle (SDL) as implemented at Microsoft recommends (see pages 42-43 of SDL Process Guidance 5.1):
“NULL out freed memory pointers in new code. This helps reduce the severity of double-free bugs and bugs that overwrite "dangling" pointers. For example:
Add this statement after the delete operator:
A common approach that we’ve seen developers take is to define macros such as
and use these in their code in preference to calling the deallocating function directly.
In light of the SDL recommendation above – and a number of real bugs related to reuse of stale references to deleted C++ objects – it is natural to look at whether we could add compiler support to do this kind of instrumentation automatically:
The obvious choice of sanitization value is NULL. However there are downsides to that: we know that a large number of application crashes are due to NULL pointer dereferences. Choosing NULL as a sanitization value would mean that new crashes introduced by this feature may be less likely to stand out to a developer as needing a proper solution – i.e. proper management of C++ object lifetimes – rather than just a NULL check that suppresses the immediate symptom.
Also checks for NULL are a common code construct meaning that an existing check for NULL combined with using NULL as a sanitization value could fortuitously hide a genuine memory safety issue whose root cause really does needs addressing.
For this reason we have chosen 0x8123 as a sanitization value – from an operating system perspective this is in the same memory page as the zero address (NULL), but an access violation at 0x8123 will better stand out to the developer as needing more detailed attention.
Finally note that if a developer actively sanitizes a pointer by writing:
and we insert an automatic sanitization instruction to some INVALID_ADDRESS:
then the optimizer would typically identify it as dead code and eliminate it, reducing it back to:
Indeed if ‘p’ is not used after deletion then the optimizer may also eliminate the “p = NULL;” instruction too! So carrying out pointer sanitization should not typically lead to performance issues.
It is certainly valid for a developer to choose to define and use macros such as SAFE_DELETE and SAFE_ARRAYDELETE above where appropriate. However getting the compiler to do this automatically is not: what if ‘p’ were to change between the “delete p;” and the “p = INVALID_ADDRESS;” instructions? Here are a couple of examples of code that would be broken by such an “always-on” sanitization.
In this example we have a class used to maintain a linked list of objects. The static class member m_head is used to keep track of the first element in the list.
This is valid code and the expected behaviour is that
would delete the memory associated with the m_head object, and update the (static) m_head class member to point to the next object in the list (m_next) which has now become the first list entry.
However the automatic pointer sanitization would break this because we would effectively have:
But m_head is no longer the object that was just deleted, as the value of Node::m_head was updated during the destructor call to point to the (presumably valid) object m_next!
2. Expressions involving a dereference: “delete p->q”
This example shows an analogous problem that can arise even without static members – the setup here is that of two objects of type CChild and CParent, each with a pointer to the other.
In this example we have two interrelated class types with a CParent object containing a pointer to its child and vice versa. Now consider the instruction:
If we applied automatic pointer sanitization, then this would be equivalent to:
However the destructor call also deletes the child member m_Child. This means that the instruction actually deletes both pChild->m_Parent and pChild itself! The memory associated with pChild is therefore freed as part of this single delete instruction and the sanitization instruction is illegal, accessing the now freed memory at pChild – as such the sanitization instruction itself could even trigger an access violation!
Thinking through the two examples above makes clear that there are many cases where the destructor call could alter or invalidate part of the expression being passed into the operator::delete call, making inserting a sanitization instruction incorrect or illegal.
If the impact of this security feature were limited to areas described in the standards as “undefined” or “implementation-dependent” then always applying pointer sanitization might be a consideration. However incompatibility with valid code constructs as described above precludes that.
Visual Studio 11 provides initial support for pointer sanitization in the following cases:
The /sdl switch is specified by the user.
The expression passed into the delete call does not involve a dereference.
No non-trivial destructor is defined.
The global operator::delete is being used (rather than any class-specific override).
Protection therefore applies to simple examples such as:
This ensures that valid code constructs such as the ones described previously are not broken (as the pointers are not sanitized in these cases)!
Automated pointer sanitization is one example where we need to take particular care in order to avoid breaking conformant C++ code.
We want to see improved software security and the tooling support that goes with that – however to be effective then that tooling must be usable on real code! The pointer sanitization support in Visual Studio 11 seeks to strike that balance. In future one might consider additional analysis to support identifying additional cases where it would be safe to sanitize.
We encourage you to try out the new /sdl compiler switch (along with other Security Development Lifecycle tools such as BinScope Binary Analyzer and Attack Surface Analyzer) and provide feedback on your experience!
Tim Burrell, MSEC Security Science team.