Aussie AI Blog

Smart Stack Buffers for Memory Safe C++

  • Oct 29th, 2024
  • by David Spuler, Ph.D.

Smart Stack Buffers for Safe C++

The idea behind smart stack buffers is to use a light wrapper around any text buffers or arrays on the stack (i.e., a local variable inside a C++ function). The idea is reasonably efficient and convenient, because:

  • inline functions make it fast (even as a class wrapper).
  • Fast tests detect buffer overflow with a single arithmetic test.
  • Destructors are automatically executed whenever it goes out-of-scope (e.g., function returns), so we don't need to track that.

Here's one way to use a class wrapper to track an automatic array buffer:

    char stackbuf[1000] = "";
    SafeBufferWrap stackwrap(stackbuf, sizeof stackbuf);

Note that this is a two-variable method: the original buffer is unchanged, but a second class object is used to check it. There are other methods whereby a class object is used instead of a text buffer variable, which we'll explore further below.

Here's the example wrapper class called SafeBufferWrap, which is initialized with the raw text buffer variable, and tracks it from the outside:

    class SafeBufferWrap {   // Safe wrapper object for char[] buffers...
    private:
	char* m_string;  // Address this buffer wrapper is tracking
	int m_bufsize;   // Number of bytes allocated (stack or wherever)
    public:
	SafeBufferWrap() = delete; // disallow without a string...
	SafeBufferWrap(char* addr, int bufsize) {  // Initialize
		ASSERT_RETURN(addr != NULL);
		m_string = addr;
		m_bufsize = bufsize;
		// Set the overrun detection sentinel byte to zero
		m_string[m_bufsize - 1] = 0;
	}
	void check_overflow() {
		// Check for buffer overrun... (at some prior time)
		if (m_string[m_bufsize - 1] != 0) {
			// Detected overflow (but don't know when)
			AUSSIE_ERROR("AUS050", "ERROR: SafeBufferWrap buffer overrun previously occurred");
		}
	}
	~SafeBufferWrap() { // Destructor
		check_overflow();
	}

	char* string() { return m_string; }
	int size() { return m_bufsize; }
    };

It's quite a lot of code, which gives it a "heavy" appearance, but note it's actually quite "light" with relative efficiency. Firstly, all the functions can be inline. Secondly, the tripwrire is to clear a single byte to null, and the test for overflow is a single byte test for non-null. That is very efficient.

Why Bother?

Why do we need this? After all, the various sanitizers such as valgrind or AddressSanitizer (ASan), can find stack buffer overflows. Well, actually valgrind cannot find stack overflows, but only allocated memory overflows, though at least ASan does detect stack variable glitches.

The reason to do this is simple: it's faster!

Since the wrapper checking is much faster than sanitizers, we can just leave it running all the time. This means that we can detect these overflows:

  • Continuous detection during testing (by developers and/or QA).
  • Can be shipped to customers enabled (either when beta testing, or maybe even in production).
  • Helps tracking down intermittent and hard-to-reproduce cases.

We can't realistically be running the sanitizers in any of those situations. I'm not saying to replace them, because it's critically important to run full sanitizers on the overall regression test suite as part of your nightly builds. We can do both.

Zeros and Canaries

There are two ways to further extend this safe buffer wrapper idea:

  • Auto-initialize the buffer to all-zeros (safety).
  • Set the buffer to "canary" bytes (triggers failures).

Note that the two ideas are mutually exclusive. We can either go for suppressing all the initialization errors, or we can intentionally set the buffer to non-zero values, so as to shake out more bugs. Less bugs, or more bugs, take your choice.

Here's the code with these two additional options:

    //---------------------------------------------------------------------------
    // -- SafeBufferWrap --
    //---------------------------------------------------------------------------
    #define SAFE_BUFFER_WRAP_CLEAR  1   // 1 will initialize all bytes to 0
    #define SAFE_BUFFER_WRAP_CANARY  1   // 1 will initialize all bytes to a canary byte


    class SafeBufferWrap {   // Safe wrapper object for char[] buffers...
	const char magicbyte = '@';
    private:
	char* m_string;  // Address this buffer wrapper is tracking
	int m_bufsize;   // Number of bytes allocated (stack or wherever)
    public:
	SafeBufferWrap() = delete; // disallow without a string...
	SafeBufferWrap(char* addr, int bufsize) {  // Initialize
		ASSERT_RETURN(addr != NULL);
		m_string = addr;
		m_bufsize = bufsize;
		// Optionally: clear all bytes to zero to avoid uninitialized errors..
    #if SAFE_BUFFER_WRAP_CLEAR
		memset(m_string, 0, m_bufsize);  // Clear buffer to zero
    #endif
    #if SAFE_BUFFER_WRAP_CANARY
		memset(m_string, magicbyte/*'@'*/, m_bufsize);  // Mark all buffer with canary bytes
    #endif
		// Set the overrun detection sentinel byte to zero
		m_string[m_bufsize - 1] = 0;
	}
	void check_overflow() {
		// Check for buffer overrun... (at some prior time)
		if (m_string[m_bufsize - 1] != 0) {
			// Detected overflow (but don't know when)
			AUSSIE_ERROR("AUS050", "ERROR: SafeBufferWrap buffer overrun previously occurred");
		}
	}
	~SafeBufferWrap() { // Destructor
		check_overflow();
	}

	char* string() { return m_string; }
	int size() { return m_bufsize; }
    };

Limitations

The limitations of this approach include:

  • After-the-fact detection of buffer overruns (we don't know when it occurred, or what code caused the overrun).
  • Does not prevent the overrun so it won't stop a crash and isn't a protection against attackers.
  • Only detects writes beyond array bounds, not reads.

Hence, we still need to do all that work to make sure that the buffers don't overrun! And we still need to run the sanitizers in auto mode while we sleep.

Related Memory Safety Blog Articles

See also these articles:

Safe C++ Book



Safe C++: Fixing Memory Safety Issues The new Safe C++ coding book by David Spuler:
  • Memory Safety
  • Rust versus C++
  • The Safe C++ Standard
  • Pragmatic Memory Safety

Get your copy from Amazon: Safe C++: Fixing Memory Safety Issues

More AI Research Topics

Read more about: