Build a Secure Memory Library in C++

Use C++ OOP to prevent buffer overflows, use-after-free, and memory leaks.

Introduction

30 Second Summary

Every time you type a password into a website, that password sits in your computer's memory. If a program does not clean up properly, attackers can scan memory and find those secrets long after you thought they were gone.

In this project, you will build a C++ secure memory library that makes common vulnerabilities structurally impossible. You will use object-oriented features like classes, destructors, and smart pointers to prevent buffer overflows, memory leaks, and data exposure.

What You'll Build

You will build a defensive security library in C++ that wraps raw memory operations in safe class abstractions, then test it against vulnerable C-style code to prove each defense works.

By the end of this project, you'll have:

  • A SecureBuffer class with bounds-checked access via operator overloading and automatic cleanup via RAII.
  • A SecureString class using inheritance that guarantees passwords are wiped from memory when they go out of scope.
  • A SmartBuffer class using smart pointers that eliminates use-after-free bugs at compile time.
  • Secret Mission: a SecureVector template class that works with any data type and includes iterator-style safe traversal.

Are there any prerequisites?

This project assumes you have basic C knowledge, including pointers, arrays, structs, and memory allocation. You do not need prior C++ experience.

Install MSYS2 and Configure VS Code

To write C++ code, you need a compiler that turns your source code into an executable program. Windows does not ship with a C++ compiler, so you need to install one.

In this step, you will install MSYS2 (a package manager for Windows that provides GCC and other development tools), then configure VS Code to recognize the compiler for IntelliSense support.

In this step, get ready to:
  • Install MSYS2 and the MinGW-w64 UCRT64 toolchain (g++, gdb).
  • Add the compiler to your Windows PATH and verify it works.
  • Install the C/C++ extension in VS Code and confirm IntelliSense detects the compiler.
Download and install MSYS2

MSYS2 is a Windows software distribution that gives you access to thousands of pre-built packages via the pacman package manager. You will use it to install the GCC C++ compiler and the GDB debugger.

  • Open your browser and navigate to msys2.org.
  • Click the download link for msys2-x86_64-20260322.exe.
  • Run the downloaded installer.
  • Leave the installation folder as the default (C:\msys64) and click through the wizard.
  • When done, click Finish.

What happens after you click Finish?

The installer automatically opens a terminal window for the UCRT64 environment. This is where you will run the next command to install the compiler toolchain.

  • Install the MinGW-w64 UCRT64 toolchain (includes g++ and gdb) by running this command in the MSYS2 terminal that just opened:
pacman -S --needed base-devel mingw-w64-ucrt-x86_64-toolchain

What will pacman ask you?

Pacman will show a list of packages in the toolchain group and ask you to choose which packages to install. Press Enter to accept the default (all packages).

Next, pacman will show the total download size and ask you to confirm the installation. Press Enter again to start the download.

  • Wait for the installation to complete. This may take a few minutes depending on your internet speed.
  • Once finished, close the MSYS2 terminal window.

Seeing download errors?

If pacman fails to download packages, your firewall or antivirus may be blocking the connection. Try temporarily disabling your firewall, or connect to a different network and run the command again.

help me fix pacman download failures in MSYS2

Add the compiler to your Windows PATH

The UCRT64 tools (g++, gdb) live inside C:\msys64\ucrt64\bin. Windows needs to know where to find them when you type a command in any terminal outside of MSYS2.

  • Press the Windows key to open your search bar.
  • Type environment variables and select Edit environment variables for your account.
  • In the User variables section, select Path and click Edit.
  • Click New and paste this path:
C:\msys64\ucrt64\bin
  • Click OK to close the Edit dialog, then OK again to close the Environment Variables window.
  • Open a brand new Command Prompt (existing terminals will not pick up PATH changes).
  • Verify g++ is accessible by running this command:
g++ --version

✔️ I see a version number

You should see output showing the GCC version (such as 16.1.0) and details about the MSYS2 build. Your compiler is ready.

ⓧ Command not found

If you see a message saying that g++ is not recognized as a command, the PATH was not updated correctly.

  • Re-open the Environment Variables editor and confirm C:\msys64\ucrt64\bin is listed under your Path variable.
  • Make sure you opened a new Command Prompt after saving the change. Old terminals do not see updated PATH values.
  • Verify the file exists by checking C:\msys64\ucrt64\bin\g++.exe in File Explorer.
  • Also verify gdb is accessible by running:
gdb --version

You should see output confirming the GNU Debugger is installed.

Why UCRT64 instead of MINGW64?

MSYS2 offers multiple environments with different C runtime libraries. UCRT64 uses the Universal C Runtime, which is the modern default on Windows 10 and newer.

The older MINGW64 environment was deprecated in March 2026. UCRT64 has been the recommended default since October 2022.

Install the C/C++ extension in VS Code

The C/C++ extension provides IntelliSense (autocomplete, error highlighting, and go-to-definition) for your C++ code. It does not include a compiler. It connects to the g++ you just installed.

  • Switch back to VS Code.
  • Open the Extensions sidebar by pressing Ctrl+Shift+X.
  • Type C/C++ in the search bar.
  • Find the extension published by Microsoft (ID: ms-vscode.cpptools) and click Install.
  • Wait for the installation to finish (a reload may be prompted).

IntelliSense automatically scans common paths for installed compilers. Because you added C:\msys64\ucrt64\bin to your PATH, the extension detects g++ without additional configuration.

  • Verify the extension detected your compiler by pressing Ctrl+Shift+P to open the Command Palette.
  • Type Select IntelliSense Configuration and press Enter.
  • Confirm you see a compiler option referencing g++ or gcc in the dropdown list.

IntelliSense not detecting g++?

Make sure VS Code was opened (or reloaded) after you updated your PATH. The extension reads PATH at startup.

  • Try closing VS Code completely and reopening it.
  • Open a VS Code terminal (Ctrl+`) and run g++ --version to confirm the PATH is visible from inside VS Code.

help me get VS Code IntelliSense to detect my g++ compiler

Your development environment is locked and loaded. Next up, you will write some intentionally vulnerable C-style code to see exactly what happens when memory operations have no safety rails.

Write Vulnerable C-Style Code

Before building defensive C++ code, you need to understand what you're defending against. Memory vulnerabilities like buffer overflows and use-after-free are among the most exploited bug classes in real-world software.

In this step, you'll write intentionally vulnerable C-style code that demonstrates three critical memory flaws. Seeing these failures firsthand makes the C++ defenses you build next feel essential.

In this step, get ready to:
  • Create the project folder and write a buffer overflow demonstration.
  • Add use-after-free and memory leak examples.
  • Compile and run to observe the vulnerable behaviors.
Create the project folder and buffer overflow demo

A buffer overflow happens when code writes data beyond the boundaries of an allocated memory region. In C, there's nothing stopping this from happening since functions like strcpy perform no bounds checking.

  • In VS Code, open a terminal by pressing Ctrl+` (backtick key, top-left of your keyboard).
  • Navigate to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir secure-memory-lib
cd secure-memory-lib
  • Confirm the folder exists by running:
ls

You should see an empty directory listing since the folder is brand new.

  • Open this folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the secure-memory-lib folder, and click Select Folder.

VS Code reloads with your project folder as the workspace root.

  • Open a new terminal in VS Code by pressing Ctrl+`.
  • Create the file vulnerable.cpp by right-clicking in the Explorer sidebar and selecting New File.
  • Name the file vulnerable.cpp and press Enter.

Now you'll write the first vulnerability demonstration. This function creates a tiny 8-byte buffer, then shows what would happen if you tried to copy a much larger string into it.

  • In vulnerable.cpp, paste the following code:
#include <iostream>
#include <cstring>
#include <cstdint>

void buffer_overflow_demo() {
    std::cout << "=== Buffer Overflow Demo (C-style) ===" << std::endl;
    char buffer[8];
    const char* dangerous_input = "This string is way too long for the buffer!";

    std::cout << "Buffer size: 8 bytes" << std::endl;
    std::cout << "Input size: " << strlen(dangerous_input) << " bytes" << std::endl;
    std::cout << "Writing beyond buffer bounds..." << std::endl;

    // VULNERABLE: no bounds checking
    // strcpy(buffer, dangerous_input);  // Would cause undefined behavior
    std::cout << "[SKIPPED] strcpy would corrupt memory here." << std::endl;
    std::cout << "In real code, this overwrites adjacent stack variables." << std::endl;
    std::cout << std::endl;
}

int main() {
    std::cout << "*** VULNERABLE C-STYLE CODE DEMONSTRATIONS ***" << std::endl;
    std::cout << "These show what goes wrong WITHOUT C++ safety features." << std::endl;
    std::cout << std::endl;

    buffer_overflow_demo();

    std::cout << "*** All demos complete. Now build the secure versions! ***" << std::endl;
    return 0;
}

What does this code do?

The buffer_overflow_demo() function allocates only 8 bytes on the stack, then points to a 44-byte string. The actual strcpy call is commented out because running it would cause undefined behavior and potentially crash your program.

In real attacks, this overflow overwrites adjacent memory like return addresses or function pointers. An attacker can exploit this to redirect program execution to their own code.

  • Save vulnerable.cpp by pressing Ctrl+S.

Don't see the file in the sidebar?

Make sure you created the file inside the secure-memory-lib folder, not at the Desktop level. If the Explorer sidebar is collapsed, click the top icon in the left activity bar to expand it.

Help me find my file in VS Code

Add use-after-free and memory leak examples

A use-after-free vulnerability occurs when code accesses memory that has already been deallocated. The pointer still holds the old address, but that memory could now belong to something else entirely.

  • In vulnerable.cpp, find the line that says int main() { and paste the following function directly above it:
void use_after_free_demo() {
    std::cout << "=== Use-After-Free Demo (C-style) ===" << std::endl;
    int* ptr = new int(42);
    std::cout << "Allocated int with value: " << *ptr << std::endl;
    std::cout << "Address: " << ptr << std::endl;

    delete ptr;
    std::cout << "Memory freed." << std::endl;

    // VULNERABLE: accessing freed memory
    std::cout << "Accessing freed pointer (undefined behavior): " << *ptr << std::endl;
    std::cout << "This may print garbage, crash, or appear to work." << std::endl;
    std::cout << "An attacker can exploit this to control program flow." << std::endl;
    std::cout << std::endl;
}

What does this code do?

This function allocates an integer on the heap with new, immediately frees it with delete, then reads from the now-dangling pointer. The result is undefined behavior.

On some systems it may print the old value (42), on others it prints garbage, and on others it crashes. Even if it appears to work, this is a critical vulnerability that attackers can exploit to hijack execution.

A memory leak happens when allocated memory is never freed. In a long-running service, leaked memory accumulates until the system runs out.

  • Still in vulnerable.cpp, find the int main() { line again and paste this function directly above it:
void memory_leak_demo() {
    std::cout << "=== Memory Leak Demo (C-style) ===" << std::endl;
    for (int i = 0; i < 5; i++) {
        int* leak = new int[100];
        leak[0] = i;
        std::cout << "Allocated 400 bytes (iteration " << i << "), never freed." << std::endl;
        // VULNERABLE: no delete[], memory is leaked
    }
    std::cout << "2000 bytes leaked. In a long-running service, this exhausts memory." << std::endl;
    std::cout << std::endl;
}

What does this code do?

Each loop iteration allocates 100 integers (400 bytes) with new int[100] but never calls delete[] to release them. After 5 iterations, 2000 bytes are permanently lost.

In a web server handling thousands of requests, this pattern would exhaust all available memory within hours.

Now update main() to call all three demonstrations.

  • Inside main(), find this line:
    buffer_overflow_demo();
  • Add the two new function calls directly below it, so the section looks like this:
    buffer_overflow_demo();
    use_after_free_demo();
    memory_leak_demo();
  • Save vulnerable.cpp by pressing Ctrl+S.

Seeing red squiggly lines?

If IntelliSense shows errors, check that use_after_free_demo() and memory_leak_demo() are placed above main(), not inside it or below it. In C++, functions must be declared before they are called.

Help me fix IntelliSense errors in my vulnerable.cpp

Compile and run to observe the vulnerable behavior

Now you'll compile the code with g++ and run it to see how these vulnerabilities manifest at runtime. The undefined behavior in the use-after-free demo means your output may look slightly different from another learner's.

  • Compile vulnerable.cpp by running this command in your VS Code terminal:
g++ -std=c++17 -Wall -Wextra -o vulnerable vulnerable.cpp

What does this command do?

  • g++ invokes the C++ compiler you installed with MSYS2.
  • -std=c++17 tells the compiler to use the C++17 standard.
  • -Wall -Wextra enables extra warnings to catch potential issues.
  • -o vulnerable names the output executable vulnerable.exe.
  • Run the compiled program:
.\vulnerable.exe

You should see output from all three demonstrations printed to your terminal. The buffer overflow section explains what would happen if the strcpy was uncommented. The use-after-free section prints a value from freed memory (which may be 42, garbage, or cause a crash).

Why does the use-after-free sometimes appear to work?

When you access freed memory, the operating system doesn't immediately reclaim or overwrite that address. The old value may still be sitting there, making the code appear to work correctly.

This is what makes use-after-free so dangerous. The bug passes testing but an attacker can trigger specific allocation patterns that place their controlled data at that address, hijacking your program.

Seeing 'g++ is not recognized'?

This means the MSYS2 UCRT64 bin path is not in your system PATH, or your terminal was opened before you added it. Close your terminal and open a fresh one with Ctrl+` so it picks up the updated PATH variable.

Help me fix g++ not being recognized in my terminal

✔️ Awesome, I've got everything!

Great. Make sure you've saved vulnerable.cpp and that your output shows all three vulnerability demonstrations running.

ⓧ I'd like to double check the full code

Here is the complete vulnerable.cpp file. Compare it with yours to spot any differences.

#include <iostream>
#include <cstring>
#include <cstdint>

void buffer_overflow_demo() {
    std::cout << "=== Buffer Overflow Demo (C-style) ===" << std::endl;
    char buffer[8];
    const char* dangerous_input = "This string is way too long for the buffer!";

    std::cout << "Buffer size: 8 bytes" << std::endl;
    std::cout << "Input size: " << strlen(dangerous_input) << " bytes" << std::endl;
    std::cout << "Writing beyond buffer bounds..." << std::endl;

    // VULNERABLE: no bounds checking
    // strcpy(buffer, dangerous_input);  // Would cause undefined behavior
    std::cout << "[SKIPPED] strcpy would corrupt memory here." << std::endl;
    std::cout << "In real code, this overwrites adjacent stack variables." << std::endl;
    std::cout << std::endl;
}

void use_after_free_demo() {
    std::cout << "=== Use-After-Free Demo (C-style) ===" << std::endl;
    int* ptr = new int(42);
    std::cout << "Allocated int with value: " << *ptr << std::endl;
    std::cout << "Address: " << ptr << std::endl;

    delete ptr;
    std::cout << "Memory freed." << std::endl;

    // VULNERABLE: accessing freed memory
    std::cout << "Accessing freed pointer (undefined behavior): " << *ptr << std::endl;
    std::cout << "This may print garbage, crash, or appear to work." << std::endl;
    std::cout << "An attacker can exploit this to control program flow." << std::endl;
    std::cout << std::endl;
}

void memory_leak_demo() {
    std::cout << "=== Memory Leak Demo (C-style) ===" << std::endl;
    for (int i = 0; i < 5; i++) {
        int* leak = new int[100];
        leak[0] = i;
        std::cout << "Allocated 400 bytes (iteration " << i << "), never freed." << std::endl;
        // VULNERABLE: no delete[], memory is leaked
    }
    std::cout << "2000 bytes leaked. In a long-running service, this exhausts memory." << std::endl;
    std::cout << std::endl;
}

int main() {
    std::cout << "*** VULNERABLE C-STYLE CODE DEMONSTRATIONS ***" << std::endl;
    std::cout << "These show what goes wrong WITHOUT C++ safety features." << std::endl;
    std::cout << std::endl;

    buffer_overflow_demo();
    use_after_free_demo();
    memory_leak_demo();

    std::cout << "*** All demos complete. Now build the secure versions! ***" << std::endl;
    return 0;
}

You've now seen three of the most common memory vulnerabilities in action. Next up, you'll build a SecureBuffer class that makes buffer overflows structurally impossible through bounds-checked access and automatic cleanup.

Build the SecureBuffer Class with RAII

You saw how C-style code crumbles under buffer overflows, use-after-free, and memory leaks. Now you will build the solution: a class that makes those vulnerabilities structurally impossible.

The key design pattern is RAII (Resource Acquisition Is Initialization). The constructor acquires the resource (allocates memory), and the destructor releases it (wipes and frees memory). This means the resource lifetime is tied to the object lifetime, so forgetting to clean up becomes impossible.

In this step, get ready to:
  • Create a SecureBuffer class that allocates and frees memory automatically via RAII.
  • Add bounds-checked access with operator overloading so out-of-bounds reads and writes throw exceptions instead of corrupting memory.
  • Test the class by triggering the same attacks that crashed the C code and confirm they are caught safely.
Create secure_buffer.h with the RAII constructor and destructor

The constructor and destructor form the heart of RAII. The constructor allocates a zeroed-out memory buffer, and the destructor securely wipes every byte before freeing it.

  • Create a new file called secure_buffer.h in your project folder.
  • Add the following code to start the class with its RAII core:
#ifndef SECURE_BUFFER_H
#define SECURE_BUFFER_H

#include <cstdint>
#include <cstddef>
#include <stdexcept>
#include <cstring>
#include <iostream>

class SecureBuffer {
protected:
    uint8_t* data_;
    size_t size_;

public:
    // Constructor: allocate and zero-initialize (RAII - acquire resource)
    explicit SecureBuffer(size_t size) : data_(nullptr), size_(size) {
        if (size == 0) {
            throw std::invalid_argument("Buffer size must be greater than 0");
        }
        data_ = new uint8_t[size];
        std::memset(data_, 0, size_);
    }

    // Destructor: securely wipe and free (RAII - release resource)
    virtual ~SecureBuffer() {
        if (data_) {
            // Basic secure wipe using volatile to prevent optimization
            volatile uint8_t* volatile_ptr = data_;
            for (size_t i = 0; i < size_; i++) {
                volatile_ptr[i] = 0;
            }
            delete[] data_;
            data_ = nullptr;
        }
    }

    // Delete copy constructor and assignment (prevent double-free)
    SecureBuffer(const SecureBuffer&) = delete;
    SecureBuffer& operator=(const SecureBuffer&) = delete;

    // Move constructor
    SecureBuffer(SecureBuffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // Move assignment
    SecureBuffer& operator=(SecureBuffer&& other) noexcept {
        if (this != &other) {
            if (data_) {
                volatile uint8_t* volatile_ptr = data_;
                for (size_t i = 0; i < size_; i++) {
                    volatile_ptr[i] = 0;
                }
                delete[] data_;
            }
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

What does this code do?

The #ifndef / #define / #endif pattern is a header guard. It prevents the file from being included twice in the same compilation, which would cause duplicate definition errors.

The constructor uses new uint8_t[size] to allocate raw bytes, then memset zeroes them out. This is the "acquisition" half of RAII.

The destructor uses a volatile pointer to wipe every byte to zero before freeing. The volatile keyword prevents the compiler from optimizing away the zeroing loop, which it would normally do because the memory is about to be freed anyway. This is the same technique behind Windows' SecureZeroMemory function.

Copy operations are deleted to prevent two objects from owning the same memory (which would cause a double-free). Move operations transfer ownership safely by nulling out the source pointer.

  • Keep secure_buffer.h open. You have not closed the class yet.
Implement bounds-checked operator[] and write()

In the vulnerable C code, nothing stopped you from reading or writing past the end of a buffer. Operator overloading lets you make the [] syntax perform a bounds check every single time.

  • Add the following code directly below the move assignment operator in secure_buffer.h:
    // Bounds-checked element access (operator overloading)
    uint8_t& operator[](size_t index) {
        if (index >= size_) {
            throw std::out_of_range("SecureBuffer: index out of bounds");
        }
        return data_[index];
    }

    const uint8_t& operator[](size_t index) const {
        if (index >= size_) {
            throw std::out_of_range("SecureBuffer: index out of bounds");
        }
        return data_[index];
    }

    // Bounds-checked write
    void write(const uint8_t* src, size_t len, size_t offset = 0) {
        if (offset + len > size_) {
            throw std::out_of_range("SecureBuffer::write: would exceed buffer bounds");
        }
        std::memcpy(data_ + offset, src, len);
    }

    size_t size() const { return size_; }

    // Print buffer contents (for demonstration)
    void print_hex(const std::string& label) const {
        std::cout << label << " [" << size_ << " bytes]: ";
        for (size_t i = 0; i < size_ && i < 32; i++) {
            printf("%02x ", data_[i]);
        }
        if (size_ > 32) std::cout << "...";
        std::cout << std::endl;
    }
};

#endif // SECURE_BUFFER_H

What does this code do?

The two operator[] overloads (one non-const, one const) let you use familiar array syntax like buf[3] while secretly performing a bounds check every time. If the index is out of range, the code throws std::out_of_range instead of silently corrupting memory.

The write() method validates that the incoming data fits within the buffer before copying. This directly prevents the buffer overflow vulnerability from vulnerable.cpp.

The print_hex() helper displays buffer contents as hexadecimal bytes so you can visually inspect memory during testing.

  • Save secure_buffer.h.

Seeing red squiggles in VS Code?

If IntelliSense shows errors, make sure the closing }; for the class and #endif are both present at the bottom of the file. A missing semicolon after the class brace is the most common C++ header error.

help me fix red squiggles in my secure_buffer.h file

✔️ Awesome, I've got everything!

Great. Double check you have saved secure_buffer.h and the file has no red error indicators.

ⓧ I'd like to double check the full code

#ifndef SECURE_BUFFER_H
#define SECURE_BUFFER_H

#include <cstdint>
#include <cstddef>
#include <stdexcept>
#include <cstring>
#include <iostream>

class SecureBuffer {
protected:
    uint8_t* data_;
    size_t size_;

public:
    // Constructor: allocate and zero-initialize (RAII - acquire resource)
    explicit SecureBuffer(size_t size) : data_(nullptr), size_(size) {
        if (size == 0) {
            throw std::invalid_argument("Buffer size must be greater than 0");
        }
        data_ = new uint8_t[size];
        std::memset(data_, 0, size_);
    }

    // Destructor: securely wipe and free (RAII - release resource)
    virtual ~SecureBuffer() {
        if (data_) {
            // Basic secure wipe using volatile to prevent optimization
            volatile uint8_t* volatile_ptr = data_;
            for (size_t i = 0; i < size_; i++) {
                volatile_ptr[i] = 0;
            }
            delete[] data_;
            data_ = nullptr;
        }
    }

    // Delete copy constructor and assignment (prevent double-free)
    SecureBuffer(const SecureBuffer&) = delete;
    SecureBuffer& operator=(const SecureBuffer&) = delete;

    // Move constructor
    SecureBuffer(SecureBuffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // Move assignment
    SecureBuffer& operator=(SecureBuffer&& other) noexcept {
        if (this != &other) {
            if (data_) {
                volatile uint8_t* volatile_ptr = data_;
                for (size_t i = 0; i < size_; i++) {
                    volatile_ptr[i] = 0;
                }
                delete[] data_;
            }
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    // Bounds-checked element access (operator overloading)
    uint8_t& operator[](size_t index) {
        if (index >= size_) {
            throw std::out_of_range("SecureBuffer: index out of bounds");
        }
        return data_[index];
    }

    const uint8_t& operator[](size_t index) const {
        if (index >= size_) {
            throw std::out_of_range("SecureBuffer: index out of bounds");
        }
        return data_[index];
    }

    // Bounds-checked write
    void write(const uint8_t* src, size_t len, size_t offset = 0) {
        if (offset + len > size_) {
            throw std::out_of_range("SecureBuffer::write: would exceed buffer bounds");
        }
        std::memcpy(data_ + offset, src, len);
    }

    size_t size() const { return size_; }

    // Print buffer contents (for demonstration)
    void print_hex(const std::string& label) const {
        std::cout << label << " [" << size_ << " bytes]: ";
        for (size_t i = 0; i < size_ && i < 32; i++) {
            printf("%02x ", data_[i]);
        }
        if (size_ > 32) std::cout << "...";
        std::cout << std::endl;
    }
};

#endif // SECURE_BUFFER_H
Create main.cpp to test SecureBuffer

Time to throw the same attacks at your SecureBuffer that destroyed the C-style code. You will attempt an out-of-bounds read and an overflow write, and confirm both are caught by exceptions instead of corrupting memory.

  • Create a new file called main.cpp in your project folder.
  • Add the following test code:
#include <iostream>
#include "secure_buffer.h"

void test_bounds_checking() {
    std::cout << "=== TEST: Bounds-Checked Access (SecureBuffer) ===" << std::endl;
    SecureBuffer buf(16);

    // Write some data safely
    const uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF};
    buf.write(data, 4);
    buf.print_hex("After write");

    // Safe access within bounds
    std::cout << "buf[0] = 0x" << std::hex << (int)buf[0] << std::dec << " (safe)" << std::endl;

    // Attempt out-of-bounds access
    try {
        uint8_t val = buf[100]; // Out of bounds!
        (void)val;
        std::cout << "ERROR: Should not reach here!" << std::endl;
    } catch (const std::out_of_range& e) {
        std::cout << "CAUGHT: " << e.what() << std::endl;
        std::cout << "DEFENSE: Buffer overflow PREVENTED by bounds checking." << std::endl;
    }

    // Attempt overflow write
    try {
        const uint8_t big_data[100] = {};
        buf.write(big_data, 100); // Too large!
    } catch (const std::out_of_range& e) {
        std::cout << "CAUGHT: " << e.what() << std::endl;
        std::cout << "DEFENSE: Overflow write PREVENTED." << std::endl;
    }

    std::cout << std::endl;
}

int main() {
    std::cout << "*** SecureBuffer Test ***" << std::endl;
    std::cout << std::endl;

    test_bounds_checking();

    return 0;
}

What does this test do?

The test creates a 16-byte SecureBuffer, writes 4 bytes safely, then deliberately tries two attacks. First, it reads at index 100 (far past the 16-byte boundary). Second, it tries to write 100 bytes into the 16-byte buffer.

Both attacks are wrapped in try/catch blocks. Instead of silently corrupting memory like the C version would, your SecureBuffer throws std::out_of_range and the program handles it gracefully.

  • Save main.cpp.
  • Compile and run the test by running this command:
g++ -std=c++17 -Wall -Wextra -o main main.cpp && .\main.exe

You should see output showing the hex data written successfully, followed by two "CAUGHT" messages confirming both attacks were blocked.

What should I see?

Your terminal output should include lines like:

  • After write [16 bytes]: de ad be ef 00 00... showing your 4 bytes were written safely.
  • CAUGHT: SecureBuffer: index out of bounds proving the out-of-bounds read was intercepted.
  • CAUGHT: SecureBuffer::write: would exceed buffer bounds proving the overflow write was intercepted.

Seeing a compile error?

If you see an error mentioning secure_buffer.h: No such file or directory, make sure both main.cpp and secure_buffer.h are in the same folder. The #include "secure_buffer.h" directive looks in the current directory first.

If you see errors about missing semicolons or braces, open secure_buffer.h and confirm the class ends with }; followed by #endif.

help me fix a compile error in my SecureBuffer project

Why volatile for secure wiping?

Without volatile, the compiler sees that you are zeroing memory right before freeing it and thinks "this write has no observable effect, I will skip it." The volatile keyword tells the compiler that every access to that pointer matters and must not be optimized away.

This is the same principle that Microsoft's SecureZeroMemory uses under the hood. In production security code, you would use platform-specific APIs. Here you are learning the underlying mechanism so you understand why they exist.

Your SecureBuffer now makes buffer overflows and memory leaks structurally impossible through RAII and bounds checking. Next up, you will build SecureString using inheritance to guarantee that sensitive data like passwords is wiped from memory the instant it is no longer needed.

Build SecureString with Inheritance and Secure Wiping

Your SecureBuffer class prevents buffer overflows and memory leaks through bounds checking and RAII. But what about sensitive data like passwords?

Even after memory is freed, the old data can linger in RAM until something else overwrites it. An attacker scanning process memory can recover passwords that were "deleted" minutes ago. In this step, you will build a SecureString class that inherits all of SecureBuffer's protections and adds guaranteed memory wiping when the string goes out of scope.

In this step, get ready to:
  • Create a SecureString class that inherits from SecureBuffer.
  • Override the destructor to log and trigger secure memory wiping.
  • Test that passwords are erased from memory on destruction.
Create the SecureString class with inheritance

Inheritance lets you build a new class on top of an existing one, reusing all its features without rewriting them. Your SecureString will inherit bounds checking, RAII cleanup, and the volatile wipe from SecureBuffer automatically.

  • Create a new file called secure_string.h in your project folder.
  • Add the following code to define the SecureString class with its constructor and accessor methods:
#ifndef SECURE_STRING_H
#define SECURE_STRING_H

#include "secure_buffer.h"
#include <cstring>

class SecureString : public SecureBuffer {
public:
    // Constructor: store a string securely
    explicit SecureString(const char* str)
        : SecureBuffer(std::strlen(str) + 1) {
        std::memcpy(data_, str, size_);
    }

    // Get the stored string (read-only access)
    const char* c_str() const {
        return reinterpret_cast<const char*>(data_);
    }

    size_t length() const {
        return size_ - 1; // Exclude null terminator
    }
};

#endif // SECURE_STRING_H

What does this code do?

The : public SecureBuffer syntax means SecureString inherits everything from SecureBuffer. It automatically gets bounds-checked access, the volatile wipe destructor, and move semantics.

The constructor calls SecureBuffer(std::strlen(str) + 1) to allocate exactly enough bytes for the string plus its null terminator. Then std::memcpy copies the password into the protected data_ buffer.

The c_str() method casts the raw bytes back to a readable string. The length() method returns the string length excluding the null terminator.

  • Save secure_string.h.
Override the destructor for secure wiping

When a SecureString is destroyed, you want visible proof that the sensitive data is being wiped. C++ calls the derived class destructor first, then the base class destructor. This means your override can log a message, and then SecureBuffer's destructor handles the actual volatile-pointer zeroing automatically.

  • In secure_string.h, find the length() method.
  • Add the following destructor and is_wiped() method below it, inside the public: section (before the closing };):
    // Destructor: secure wipe is inherited from SecureBuffer
    // The base class destructor handles wiping via volatile pointer
    ~SecureString() override {
        std::cout << "[SecureString] Destructor called - wiping "
                  << size_ << " bytes of sensitive data" << std::endl;
        // Base class destructor will do the actual secure wipe
    }

    // Check if memory is zeroed (for testing)
    bool is_wiped() const {
        for (size_t i = 0; i < size_; i++) {
            if (data_[i] != 0) return false;
        }
        return true;
    }

How does the destruction chain work?

The override keyword tells the compiler this destructor replaces the base class's virtual destructor for SecureString objects. When a SecureString goes out of scope, C++ runs the derived destructor first (the log message), then automatically calls SecureBuffer's destructor (the volatile-pointer wipe).

The volatile pointer pattern in the base class prevents the compiler from optimizing away the zeroing. Without volatile, the compiler sees "writing zeros to memory we are about to free" and may skip it as a dead store. Volatile tells the compiler every write must actually happen.

The is_wiped() method checks every byte in the buffer, returning true only if all bytes are zero. You will use this in your test to prove the wipe happened.

  • Save secure_string.h.

Seeing a compile error about std::cout?

The secure_string.h file uses std::cout in the destructor. This works because secure_buffer.h (which you include at the top) already includes <iostream>. Make sure your #include "secure_buffer.h" line is at the top of the file.

help me fix a compile error in secure_string.h

✔️ Awesome, I've got everything!

Great. Make sure you have saved secure_string.h before continuing.

ⓧ I'd like to double check the full code

#ifndef SECURE_STRING_H
#define SECURE_STRING_H

#include "secure_buffer.h"
#include <cstring>

class SecureString : public SecureBuffer {
public:
    // Constructor: store a string securely
    explicit SecureString(const char* str)
        : SecureBuffer(std::strlen(str) + 1) {
        std::memcpy(data_, str, size_);
    }

    // Destructor: secure wipe is inherited from SecureBuffer
    // The base class destructor handles wiping via volatile pointer
    ~SecureString() override {
        std::cout << "[SecureString] Destructor called - wiping "
                  << size_ << " bytes of sensitive data" << std::endl;
        // Base class destructor will do the actual secure wipe
    }

    // Get the stored string (read-only access)
    const char* c_str() const {
        return reinterpret_cast<const char*>(data_);
    }

    size_t length() const {
        return size_ - 1; // Exclude null terminator
    }

    // Check if memory is zeroed (for testing)
    bool is_wiped() const {
        for (size_t i = 0; i < size_; i++) {
            if (data_[i] != 0) return false;
        }
        return true;
    }
};

#endif // SECURE_STRING_H
Test that passwords are wiped from memory

Now you will prove that SecureString actually zeroes out password data when the object is destroyed. The test stores a password, prints its memory contents, then lets it go out of scope so the destructor fires.

  • In main.cpp, add the following include at the top of the file, below the existing #include "secure_buffer.h" line:
#include "secure_string.h"
  • Add the following test function above your main() function:
void test_secure_wiping() {
    std::cout << "=== TEST: Secure Memory Wiping (SecureString) ===" << std::endl;
    const char* secret = "SuperSecret123!";

    {
        SecureString password(secret);
        std::cout << "Stored password: " << password.c_str() << std::endl;
        std::cout << "Password length: " << password.length() << std::endl;
        password.print_hex("Password memory");
        // password goes out of scope here - destructor wipes memory
    }

    std::cout << "Password object destroyed." << std::endl;
    std::cout << "DEFENSE: Sensitive data wiped from memory via RAII destructor." << std::endl;
    std::cout << "An attacker scanning memory will NOT find the password." << std::endl;
    std::cout << std::endl;
}

What does this test do?

The inner braces { } create a limited scope. When the closing brace is reached, password goes out of scope and its destructor fires automatically.

You will see the [SecureString] Destructor called log message appear, proving the wipe triggered. The print_hex() call before destruction shows the raw password bytes in memory, confirming the data was stored.

  • In your main() function, add a call to test_secure_wiping() after the existing test_bounds_checking() call:
    test_secure_wiping();
  • Save main.cpp.
  • Compile and run the updated program by running this command:
g++ -std=c++17 -Wall -Wextra -o main main.cpp
  • Run the program:
./main

You should see the SecureBuffer bounds-checking test from before, followed by the new secure wiping test. Look for the [SecureString] Destructor called - wiping 16 bytes of sensitive data message confirming the password was wiped on destruction.

Why 16 bytes?

The password SuperSecret123! is 15 characters long. The constructor adds 1 byte for the null terminator (\0), giving 16 bytes total. This is the same pattern C strings use, but now wrapped in a class that guarantees cleanup.

Don't see the destructor message?

Make sure the inner braces { } around the SecureString password(secret) line are present. Without them, the password object stays alive until the end of the function and the destructor message appears later than expected.

Also confirm you included #include "secure_string.h" at the top of main.cpp.

help me debug why my SecureString destructor message is not appearing

Your SecureString now guarantees that passwords are zeroed from memory the instant they are no longer needed. Next, you will tackle use-after-free bugs using smart pointers that make dangling pointer access impossible at compile time.

Eliminate Use-After-Free with Smart Pointers

Your SecureBuffer already prevents buffer overflows and memory leaks. But it still requires you to manually write move constructors and move assignment operators to handle ownership transfers safely.

C++ provides std::unique_ptr, a smart pointer that handles ownership automatically. When you wrap memory in a unique_ptr, the compiler enforces single-ownership rules for you. Copying is blocked at compile time, and use-after-free becomes structurally impossible.

In this step, get ready to:
  • Create a SmartBuffer class that wraps memory in std::unique_ptr.
  • Demonstrate move-only semantics that prevent copying.
  • Run a comparison test showing SmartBuffer catching unsafe access.
Create the SmartBuffer class with std::unique_ptr

A use-after-free bug happens when code accesses memory after it has been freed. In C, nothing stops you from using a dangling pointer. std::unique_ptr solves this by making ownership explicit. Only one unique_ptr can own a piece of memory at a time.

  • Create a new file called smart_buffer.h in your project folder by running this command in your terminal:
touch smart_buffer.h
  • Open smart_buffer.h in VS Code and paste the following code:
#ifndef SMART_BUFFER_H
#define SMART_BUFFER_H

#include <memory>
#include <cstdint>
#include <cstddef>
#include <stdexcept>
#include <iostream>
#include <cstring>

class SmartBuffer {
private:
    std::unique_ptr<uint8_t[]> data_;
    size_t size_;

public:
    explicit SmartBuffer(size_t size) : data_(std::make_unique<uint8_t[]>(size)), size_(size) {
        std::memset(data_.get(), 0, size_);
    }

    // unique_ptr automatically deletes copy operations
    // SmartBuffer is move-only (like unique_ptr itself)

    // Bounds-checked access
    uint8_t& operator[](size_t index) {
        if (index >= size_) {
            throw std::out_of_range("SmartBuffer: index out of bounds");
        }
        return data_[index];
    }

    const uint8_t& operator[](size_t index) const {
        if (index >= size_) {
            throw std::out_of_range("SmartBuffer: index out of bounds");
        }
        return data_[index];
    }

    void write(const uint8_t* src, size_t len, size_t offset = 0) {
        if (offset + len > size_) {
            throw std::out_of_range("SmartBuffer::write: would exceed buffer bounds");
        }
        std::memcpy(data_.get() + offset, src, len);
    }

    size_t size() const { return size_; }

    // Check if buffer is valid (has ownership)
    bool is_valid() const { return data_ != nullptr; }

    void print_hex(const std::string& label) const {
        if (!data_) {
            std::cout << label << " [MOVED - no data]" << std::endl;
            return;
        }
        std::cout << label << " [" << size_ << " bytes]: ";
        for (size_t i = 0; i < size_ && i < 32; i++) {
            printf("%02x ", data_[i]);
        }
        if (size_ > 32) std::cout << "...";
        std::cout << std::endl;
    }
};

#endif // SMART_BUFFER_H

What does this code do?

The SmartBuffer class delegates all memory management to std::unique_ptr<uint8_t[]>. You never write new or delete yourself.

  • The constructor uses std::make_unique<uint8_t[]>(size) to allocate memory safely. No raw new needed.
  • Because unique_ptr cannot be copied, the compiler automatically deletes the copy constructor and copy assignment operator. Trying to copy a SmartBuffer causes a compile-time error.
  • The is_valid() method checks whether the buffer still owns its data. After a move, the source becomes invalid.
  • The print_hex() method safely handles the moved-from state by checking if data_ is null before accessing it.
  • Save smart_buffer.h.

Why no destructor or move operations?

Unlike SecureBuffer where you wrote a destructor, move constructor, and move assignment operator by hand, SmartBuffer has none of these. The unique_ptr member handles all of it automatically.

The compiler generates correct move operations because unique_ptr is itself moveable. This is fewer lines of code and fewer chances for bugs.

✔️ Awesome, I've got everything!

Great. Make sure you have saved smart_buffer.h before moving on.

ⓧ I'd like to double check the full code

#ifndef SMART_BUFFER_H
#define SMART_BUFFER_H

#include <memory>
#include <cstdint>
#include <cstddef>
#include <stdexcept>
#include <iostream>
#include <cstring>

class SmartBuffer {
private:
    std::unique_ptr<uint8_t[]> data_;
    size_t size_;

public:
    explicit SmartBuffer(size_t size) : data_(std::make_unique<uint8_t[]>(size)), size_(size) {
        std::memset(data_.get(), 0, size_);
    }

    // unique_ptr automatically deletes copy operations
    // SmartBuffer is move-only (like unique_ptr itself)

    // Bounds-checked access
    uint8_t& operator[](size_t index) {
        if (index >= size_) {
            throw std::out_of_range("SmartBuffer: index out of bounds");
        }
        return data_[index];
    }

    const uint8_t& operator[](size_t index) const {
        if (index >= size_) {
            throw std::out_of_range("SmartBuffer: index out of bounds");
        }
        return data_[index];
    }

    void write(const uint8_t* src, size_t len, size_t offset = 0) {
        if (offset + len > size_) {
            throw std::out_of_range("SmartBuffer::write: would exceed buffer bounds");
        }
        std::memcpy(data_.get() + offset, src, len);
    }

    size_t size() const { return size_; }

    // Check if buffer is valid (has ownership)
    bool is_valid() const { return data_ != nullptr; }

    void print_hex(const std::string& label) const {
        if (!data_) {
            std::cout << label << " [MOVED - no data]" << std::endl;
            return;
        }
        std::cout << label << " [" << size_ << " bytes]: ";
        for (size_t i = 0; i < size_ && i < 32; i++) {
            printf("%02x ", data_[i]);
        }
        if (size_ > 32) std::cout << "...";
        std::cout << std::endl;
    }
};

#endif // SMART_BUFFER_H
Demonstrate move-only semantics

Now you will write a test that proves SmartBuffer enforces single ownership. When you move data from one buffer to another, the original becomes invalid. Any attempt to use it is caught safely.

  • Open main.cpp and replace its contents with the following test code:
#include <iostream>
#include <cstring>
#include "secure_buffer.h"
#include "secure_string.h"
#include "smart_buffer.h"

void test_smart_buffer_move() {
    std::cout << "=== TEST: Use-After-Free Prevention (SmartBuffer) ===" << std::endl;

    // Create a buffer and write data into it
    SmartBuffer buf1(32);
    const uint8_t msg[] = "Hello, secure world!";
    buf1.write(msg, sizeof(msg));
    buf1.print_hex("buf1 before move");

    // Move ownership to buf2
    SmartBuffer buf2 = std::move(buf1);
    buf2.print_hex("buf2 after move");
    buf1.print_hex("buf1 after move");

    // buf1 no longer owns the data - safe to check
    std::cout << "buf1 is_valid: " << (buf1.is_valid() ? "true" : "false") << std::endl;
    std::cout << "buf2 is_valid: " << (buf2.is_valid() ? "true" : "false") << std::endl;

    // Attempting to use moved-from buffer is caught
    if (!buf1.is_valid()) {
        std::cout << "DEFENSE: Cannot access moved-from buffer (ownership transferred)." << std::endl;
        std::cout << "In C, this would be a use-after-free vulnerability." << std::endl;
    }

    std::cout << std::endl;
}

void test_dangling_pointer_comparison() {
    std::cout << "=== COMPARISON: C-style vs SmartBuffer ===" << std::endl;

    // C-style: dangling pointer after delete
    std::cout << "[C-style] Allocating and freeing memory..." << std::endl;
    int* raw_ptr = new int(42);
    std::cout << "[C-style] Value before delete: " << *raw_ptr << std::endl;
    delete raw_ptr;
    // raw_ptr is now dangling - accessing it is undefined behavior
    std::cout << "[C-style] Pointer is dangling. Accessing it = undefined behavior." << std::endl;
    std::cout << "[C-style] An attacker can exploit this to hijack control flow." << std::endl;
    std::cout << std::endl;

    // SmartBuffer: ownership transfer is safe and detectable
    std::cout << "[SmartBuffer] Creating and moving buffer..." << std::endl;
    SmartBuffer safe(16);
    safe[0] = 0xAB;
    SmartBuffer moved = std::move(safe);
    std::cout << "[SmartBuffer] After move, source is_valid: "
              << (safe.is_valid() ? "true" : "false") << std::endl;
    std::cout << "[SmartBuffer] DEFENSE: No dangling pointer. Ownership is explicit." << std::endl;
    std::cout << std::endl;
}

int main() {
    std::cout << "*** SMART POINTER TESTS ***" << std::endl;
    std::cout << std::endl;

    test_smart_buffer_move();
    test_dangling_pointer_comparison();

    std::cout << "*** All smart pointer tests complete. ***" << std::endl;
    return 0;
}

What does this test demonstrate?

  • test_smart_buffer_move() creates a buffer, writes data, then moves ownership to a second buffer. After the move, buf1.is_valid() returns false. The print_hex() method prints [MOVED - no data] instead of crashing.
  • test_dangling_pointer_comparison() shows the C-style version where a raw pointer becomes dangling after delete. Nothing in C prevents you from dereferencing it. With SmartBuffer, the invalid state is detectable programmatically.
  • Save main.cpp.

Seeing a compile error about deleted copy constructor?

If you accidentally write SmartBuffer buf2 = buf1; (without std::move), the compiler blocks you. This is the defense working. Copying a unique_ptr is not allowed because two owners would create a double-free.

Make sure you use std::move(buf1) to explicitly transfer ownership.

Compile and run the comparison test

Time to see the defense in action. You will compile main.cpp and observe how SmartBuffer handles ownership transfers safely while C-style code leaves you exposed.

  • Compile the test by running this command:
g++ -std=c++17 -Wall -Wextra -o main main.cpp
  • Run the compiled program:
./main

You should see output showing buf1 printing [MOVED - no data] after the move, and is_valid returning false. The comparison section shows how C-style code leaves a dangling pointer while SmartBuffer makes the invalid state safe and detectable.

Why std::unique_ptr over raw pointers?

With raw pointers, nothing prevents you from accessing memory after it is freed. The result is undefined behavior. An attacker can exploit this to execute arbitrary code.

With unique_ptr, ownership is tracked by the type system. After a move, the source pointer becomes nullptr. Your code can check for this explicitly instead of silently corrupting memory.

Seeing 'file not found' for smart_buffer.h?

Make sure smart_buffer.h is in the same folder as main.cpp. The #include "smart_buffer.h" directive looks in the current directory first.

Confirm by running ls in your terminal. You should see smart_buffer.h, secure_buffer.h, secure_string.h, and main.cpp all listed together.

Your SmartBuffer now makes use-after-free structurally impossible through the type system. Next up, you will combine all your secure classes into an integration test harness that proves each C++ feature maps to a specific vulnerability defense.

Build the Integration Test Harness

You have built three secure classes that each defend against a specific category of memory vulnerability. Now it is time to prove they work together as a unified defense system.

In this step, you will create a test harness that exercises every security defense in sequence. The harness deliberately triggers the same dangerous operations that crashed in your vulnerable C code, then shows how your C++ classes catch each one safely.

In this step, get ready to:
  • Write test functions that exercise bounds checking, secure wiping, use-after-free prevention, and RAII cleanup.
  • Add a summary output that maps each C++ feature to the vulnerability it prevents.
  • Compile and run the full test harness to confirm all defenses pass.
Create the test harness with security test functions

The test harness is a single file that imports all three of your secure classes and deliberately tries to break them. Each test function simulates an attack that would succeed against C-style code, proving your C++ defenses catch it.

  • Create a new file called test_harness.cpp in your project folder.
  • Add the includes and the first test function by pasting this code:
#include <iostream>
#include <cstring>
#include "secure_buffer.h"
#include "secure_string.h"
#include "smart_buffer.h"

void test_bounds_checking() {
    std::cout << "=== TEST: Bounds-Checked Access (SecureBuffer) ===" << std::endl;
    SecureBuffer buf(16);

    // Write some data safely
    const uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF};
    buf.write(data, 4);
    buf.print_hex("After write");

    // Safe access within bounds
    std::cout << "buf[0] = 0x" << std::hex << (int)buf[0] << std::dec << " (safe)" << std::endl;

    // Attempt out-of-bounds access
    try {
        uint8_t val = buf[100]; // Out of bounds!
        (void)val;
        std::cout << "ERROR: Should not reach here!" << std::endl;
    } catch (const std::out_of_range& e) {
        std::cout << "CAUGHT: " << e.what() << std::endl;
        std::cout << "DEFENSE: Buffer overflow PREVENTED by bounds checking." << std::endl;
    }

    // Attempt overflow write
    try {
        const uint8_t big_data[100] = {};
        buf.write(big_data, 100); // Too large!
    } catch (const std::out_of_range& e) {
        std::cout << "CAUGHT: " << e.what() << std::endl;
        std::cout << "DEFENSE: Overflow write PREVENTED." << std::endl;
    }

    std::cout << std::endl;
}

What does this code do?

The includes pull in all three secure classes so the harness can test each one.

  • test_bounds_checking() creates a 16-byte SecureBuffer, writes 4 bytes safely, then deliberately tries to read index 100 and write 100 bytes.
  • Both illegal operations get caught by std::out_of_range exceptions instead of silently corrupting memory.
  • Below the test_bounds_checking() function, add the secure wiping test:
void test_secure_wiping() {
    std::cout << "=== TEST: Secure Memory Wiping (SecureString) ===" << std::endl;
    const char* secret = "SuperSecret123!";

    // We use a raw pointer to observe memory after destruction
    uint8_t* raw_location = nullptr;
    size_t raw_size = 0;

    {
        SecureString password(secret);
        std::cout << "Stored password: " << password.c_str() << std::endl;
        std::cout << "Password length: " << password.length() << std::endl;
        password.print_hex("Password memory");

        // Save the address so we can inspect after destruction
        // (In real code you would NEVER do this - this is just for demonstration)
        raw_size = password.size();
        // password goes out of scope here - destructor wipes memory
    }

    std::cout << "Password object destroyed." << std::endl;
    std::cout << "DEFENSE: Sensitive data wiped from memory via RAII destructor." << std::endl;
    std::cout << "An attacker scanning memory will NOT find the password." << std::endl;
    std::cout << std::endl;
}

What does this code do?

This test stores a password in a SecureString inside a nested scope (the curly braces). When the scope ends, the destructor fires automatically and wipes the password from memory.

The raw_location and raw_size variables exist only for demonstration purposes. In production code you would never save a pointer to memory you plan to wipe.

  • Below test_secure_wiping(), add the use-after-free prevention test:
void test_use_after_free_prevention() {
    std::cout << "=== TEST: Use-After-Free Prevention (SmartBuffer) ===" << std::endl;

    SmartBuffer buf1(32);
    const uint8_t msg[] = "Hello, secure world!";
    buf1.write(msg, sizeof(msg));
    buf1.print_hex("buf1 before move");

    // Move ownership to buf2
    SmartBuffer buf2 = std::move(buf1);
    buf2.print_hex("buf2 after move");
    buf1.print_hex("buf1 after move");

    // buf1 no longer owns the data - safe to check
    std::cout << "buf1 is_valid: " << (buf1.is_valid() ? "true" : "false") << std::endl;
    std::cout << "buf2 is_valid: " << (buf2.is_valid() ? "true" : "false") << std::endl;

    // Attempting to use moved-from buffer is caught
    try {
        if (!buf1.is_valid()) {
            std::cout << "DEFENSE: Cannot access moved-from buffer (ownership transferred)." << std::endl;
            std::cout << "In C, this would be a use-after-free vulnerability." << std::endl;
        }
    } catch (...) {
        std::cout << "Unexpected error." << std::endl;
    }

    std::cout << std::endl;
}

What does this code do?

This test demonstrates move semantics with SmartBuffer. After moving buf1 into buf2, the original buffer becomes invalid.

The is_valid() check detects that ownership has transferred. In C, a dangling pointer would silently compile and crash at runtime.

  • Below test_use_after_free_prevention(), add the RAII leak prevention test:
void test_raii_no_leaks() {
    std::cout << "=== TEST: RAII Prevents Memory Leaks ===" << std::endl;

    for (int i = 0; i < 5; i++) {
        SecureBuffer buf(1024);
        buf[0] = static_cast<uint8_t>(i);
        std::cout << "Iteration " << i << ": allocated 1024 bytes, will auto-free." << std::endl;
        // buf goes out of scope here - destructor frees memory automatically
    }

    std::cout << "DEFENSE: All 5 buffers automatically freed by destructors (RAII)." << std::endl;
    std::cout << "In C, forgetting even one free() causes a leak." << std::endl;
    std::cout << std::endl;
}

What does this code do?

This test allocates a 1024-byte SecureBuffer inside a loop 5 times. Each iteration, the buffer goes out of scope and the destructor frees it automatically.

Compare this to the memory_leak_demo() in vulnerable.cpp where the same loop leaked 2000 bytes because nothing called delete[].

  • Save test_harness.cpp.

Seeing red squiggles under the includes?

Make sure secure_buffer.h, secure_string.h, and smart_buffer.h are in the same folder as test_harness.cpp. IntelliSense resolves quoted includes relative to the current file's directory.

help me fix include errors in my test harness file

Add the security defense summary

A good test harness does not just pass or fail. It should also teach the reader which C++ feature prevented which vulnerability.

  • Below test_raii_no_leaks(), add the summary function:
void print_summary() {
    std::cout << "========================================" << std::endl;
    std::cout << "   SECURITY DEFENSE SUMMARY" << std::endl;
    std::cout << "========================================" << std::endl;
    std::cout << std::endl;
    std::cout << "  C++ Feature          -> Vulnerability Prevented" << std::endl;
    std::cout << "  -------------------     ----------------------" << std::endl;
    std::cout << "  Operator overloading -> Buffer overflow" << std::endl;
    std::cout << "  RAII (destructors)   -> Memory leaks" << std::endl;
    std::cout << "  Secure wipe in dtor  -> Sensitive data residue" << std::endl;
    std::cout << "  std::unique_ptr      -> Use-after-free" << std::endl;
    std::cout << "  Deleted copy ctor    -> Double-free" << std::endl;
    std::cout << "  Inheritance          -> Code reuse for security patterns" << std::endl;
    std::cout << std::endl;
    std::cout << "Every C++ feature you learned today has a direct" << std::endl;
    std::cout << "cybersecurity application. This is how defenders think." << std::endl;
    std::cout << "========================================" << std::endl;
}

Why include a summary table?

This maps each C++ language feature you used to the specific vulnerability it prevents. Defensive security engineers think in exactly these terms: which language mechanism makes which attack structurally impossible.

Operator overloading catches buffer overflows. RAII eliminates memory leaks. std::unique_ptr prevents use-after-free. Each feature is a structural defense, not a runtime check you can forget.

  • Save test_harness.cpp.
Compile and run the full test harness

With all test functions and the summary in place, you need a main() function to tie them together. Then you will compile and run the complete harness.

  • Below print_summary(), add the main() function:
int main() {
    std::cout << "*** SECURE MEMORY LIBRARY - TEST HARNESS ***" << std::endl;
    std::cout << std::endl;

    test_bounds_checking();
    test_secure_wiping();
    test_use_after_free_prevention();
    test_raii_no_leaks();
    print_summary();

    return 0;
}

What does this code do?

The main() function calls each test in sequence: bounds checking, secure wiping, use-after-free prevention, RAII leak prevention, and finally the summary table. This order matches the progression you followed when building each class.

  • Save test_harness.cpp.
  • Compile the test harness by running this command in your terminal:
g++ -std=c++17 -Wall -Wextra -o test_harness test_harness.cpp

What does this command do?

  • -std=c++17 tells the compiler to use the C++17 standard.
  • -Wall -Wextra enables extra warnings so you catch potential issues early.
  • -o test_harness names the output executable test_harness.
  • You only need to compile test_harness.cpp because your header files are included directly by the preprocessor.
  • Run the test harness:
./test_harness
  • You should see all four tests pass, followed by the security defense summary table.
  • Look for DEFENSE: lines confirming each vulnerability was prevented.
  • The [SecureString] Destructor called message confirms the password wipe fired automatically.

Seeing a compile error?

If you see an error about a missing header file, confirm all four header files (secure_buffer.h, secure_string.h, smart_buffer.h) are in the same folder as test_harness.cpp.

If you see an undefined reference error, check that you have all the function definitions (test_bounds_checking, test_secure_wiping, test_use_after_free_prevention, test_raii_no_leaks, print_summary) before main().

help me fix compile errors in my test harness

✔️ Awesome, I've got everything!

Great. Double check you have saved test_harness.cpp and that the test harness ran successfully with all defenses showing PREVENTED in the output.

ⓧ I'd like to double check the full code

#include <iostream>
#include <cstring>
#include "secure_buffer.h"
#include "secure_string.h"
#include "smart_buffer.h"

void test_bounds_checking() {
    std::cout << "=== TEST: Bounds-Checked Access (SecureBuffer) ===" << std::endl;
    SecureBuffer buf(16);

    // Write some data safely
    const uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF};
    buf.write(data, 4);
    buf.print_hex("After write");

    // Safe access within bounds
    std::cout << "buf[0] = 0x" << std::hex << (int)buf[0] << std::dec << " (safe)" << std::endl;

    // Attempt out-of-bounds access
    try {
        uint8_t val = buf[100]; // Out of bounds!
        (void)val;
        std::cout << "ERROR: Should not reach here!" << std::endl;
    } catch (const std::out_of_range& e) {
        std::cout << "CAUGHT: " << e.what() << std::endl;
        std::cout << "DEFENSE: Buffer overflow PREVENTED by bounds checking." << std::endl;
    }

    // Attempt overflow write
    try {
        const uint8_t big_data[100] = {};
        buf.write(big_data, 100); // Too large!
    } catch (const std::out_of_range& e) {
        std::cout << "CAUGHT: " << e.what() << std::endl;
        std::cout << "DEFENSE: Overflow write PREVENTED." << std::endl;
    }

    std::cout << std::endl;
}

void test_secure_wiping() {
    std::cout << "=== TEST: Secure Memory Wiping (SecureString) ===" << std::endl;
    const char* secret = "SuperSecret123!";

    // We use a raw pointer to observe memory after destruction
    uint8_t* raw_location = nullptr;
    size_t raw_size = 0;

    {
        SecureString password(secret);
        std::cout << "Stored password: " << password.c_str() << std::endl;
        std::cout << "Password length: " << password.length() << std::endl;
        password.print_hex("Password memory");

        // Save the address so we can inspect after destruction
        // (In real code you would NEVER do this - this is just for demonstration)
        raw_size = password.size();
        // password goes out of scope here - destructor wipes memory
    }

    std::cout << "Password object destroyed." << std::endl;
    std::cout << "DEFENSE: Sensitive data wiped from memory via RAII destructor." << std::endl;
    std::cout << "An attacker scanning memory will NOT find the password." << std::endl;
    std::cout << std::endl;
}

void test_use_after_free_prevention() {
    std::cout << "=== TEST: Use-After-Free Prevention (SmartBuffer) ===" << std::endl;

    SmartBuffer buf1(32);
    const uint8_t msg[] = "Hello, secure world!";
    buf1.write(msg, sizeof(msg));
    buf1.print_hex("buf1 before move");

    // Move ownership to buf2
    SmartBuffer buf2 = std::move(buf1);
    buf2.print_hex("buf2 after move");
    buf1.print_hex("buf1 after move");

    // buf1 no longer owns the data - safe to check
    std::cout << "buf1 is_valid: " << (buf1.is_valid() ? "true" : "false") << std::endl;
    std::cout << "buf2 is_valid: " << (buf2.is_valid() ? "true" : "false") << std::endl;

    // Attempting to use moved-from buffer is caught
    try {
        if (!buf1.is_valid()) {
            std::cout << "DEFENSE: Cannot access moved-from buffer (ownership transferred)." << std::endl;
            std::cout << "In C, this would be a use-after-free vulnerability." << std::endl;
        }
    } catch (...) {
        std::cout << "Unexpected error." << std::endl;
    }

    std::cout << std::endl;
}

void test_raii_no_leaks() {
    std::cout << "=== TEST: RAII Prevents Memory Leaks ===" << std::endl;

    for (int i = 0; i < 5; i++) {
        SecureBuffer buf(1024);
        buf[0] = static_cast<uint8_t>(i);
        std::cout << "Iteration " << i << ": allocated 1024 bytes, will auto-free." << std::endl;
        // buf goes out of scope here - destructor frees memory automatically
    }

    std::cout << "DEFENSE: All 5 buffers automatically freed by destructors (RAII)." << std::endl;
    std::cout << "In C, forgetting even one free() causes a leak." << std::endl;
    std::cout << std::endl;
}

void print_summary() {
    std::cout << "========================================" << std::endl;
    std::cout << "   SECURITY DEFENSE SUMMARY" << std::endl;
    std::cout << "========================================" << std::endl;
    std::cout << std::endl;
    std::cout << "  C++ Feature          -> Vulnerability Prevented" << std::endl;
    std::cout << "  -------------------     ----------------------" << std::endl;
    std::cout << "  Operator overloading -> Buffer overflow" << std::endl;
    std::cout << "  RAII (destructors)   -> Memory leaks" << std::endl;
    std::cout << "  Secure wipe in dtor  -> Sensitive data residue" << std::endl;
    std::cout << "  std::unique_ptr      -> Use-after-free" << std::endl;
    std::cout << "  Deleted copy ctor    -> Double-free" << std::endl;
    std::cout << "  Inheritance          -> Code reuse for security patterns" << std::endl;
    std::cout << std::endl;
    std::cout << "Every C++ feature you learned today has a direct" << std::endl;
    std::cout << "cybersecurity application. This is how defenders think." << std::endl;
    std::cout << "========================================" << std::endl;
}

int main() {
    std::cout << "*** SECURE MEMORY LIBRARY - TEST HARNESS ***" << std::endl;
    std::cout << std::endl;

    test_bounds_checking();
    test_secure_wiping();
    test_use_after_free_prevention();
    test_raii_no_leaks();
    print_summary();

    return 0;
}

Secret mission

Build a SecureVector Template Class

Your secure memory library works for bytes. But what about integers, strings, or custom credential structs? Templates let you write the security logic once and apply it to any data type.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Project folder on your Desktop (secure-memory-library or similar).
  • MSYS2 installation (C:\msys64).
  • C/C++ extension for VS Code (ms-vscode.cpptools).
  • Windows PATH modification (C:\msys64\ucrt64\bin).

Keep everything running

No action needed. Choose this if you plan to continue building C++ projects with MSYS2 and g++.

  • Your project files stay on your Desktop for future reference.
  • MSYS2 and the C/C++ extension remain installed and ready for your next project.
  • The PATH entry stays configured so you can compile from any terminal.

Pause - I'll come back to this later

Free up a small amount of disk space by removing compiled executables, but keep source files and tools intact.

  • Open your project folder and delete any .exe files (vulnerable.exe, test_harness.exe, test_secure_vector.exe).
  • Keep your .cpp and .h source files. You can recompile them anytime with g++.
  • Leave MSYS2 and the VS Code extension installed so you can pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Delete your project folder. Open a Command Prompt and run:
rmdir /s /q "%USERPROFILE%\Desktop\secure-memory-library"
  • Uninstall MSYS2. Open a Command Prompt as Administrator and run:
C:\msys64\uninstall.exe pr --confirm-command
  • Remove C:\msys64\ucrt64\bin from your PATH. Open **Settings**, search for environment variables, click **Edit environment variables for your account**, select **Path**, click **Edit**, find the C:\msys64\ucrt64\bin entry, select it, and click **Delete**.
  • Uninstall the C/C++ extension. In VS Code, open the Extensions sidebar (Ctrl+Shift+X), search for C/C++, and click **Uninstall** on the Microsoft C/C++ extension.

Nice Work!

Nice Work!

You just built a secure memory library in C++ that structurally prevents real-world vulnerabilities. Every class you wrote maps directly to a defense that cybersecurity engineers use in production systems.

Here's what you learned:

  • Built a SecureBuffer class with bounds-checked access via operator overloading and automatic cleanup via RAII, preventing buffer overflows and memory leaks.
  • Built a SecureString class using inheritance and secure memory wiping via a volatile pointer pattern, preventing sensitive data residue in memory.
  • Built a SmartBuffer class using std::unique_ptr to prevent use-after-free bugs at compile time with move-only semantics.
  • Secret Mission: Built a SecureVector<T> template class with type-safe, bounds-checked, securely-wiped containers that work with any data type.

Ready to quiz yourself?