Aussie AI

Chapter 29. Smart Stack Buffers

  • Book Excerpt from "Advanced C++ Memory Techniques: Efficiency and Safety"
  • by David Spuler, Ph.D.

Chapter 29. Smart Stack Buffers

What are Smart Stack Buffers?

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.

Why Use Smart Buffers?

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

The reason to use our own smart buffers 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 development and testing (by developers and/or QA).
  • Early detection in CI/CD workflows and nightly builds.
  • Optionally, could even 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.

Two-Variable Method

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 tripwire 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.

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 of Smart Buffers

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.

 

Online: Table of Contents

PDF: Free PDF book download

Buy: Advanced C++ Memory Techniques: Efficiency and Safety

Advanced C++ Memory Techniques Advanced C++ Memory Techniques: Efficiency & Safety:
  • Memory optimization techniques
  • Memory-efficient data structures
  • DIY memory safety techniques
  • Intercepting memory primitives
  • Preventive memory safety
  • Memory reduction optimizations

Get your copy from Amazon: Advanced C++ Memory Techniques