Data Element containing Pointer Item without Proper Copy Control Element

Description

Data Element containing Pointer Item without Proper Copy Control Element occurs when code contains a data element with a pointer that does not have an associated copy constructor or assignment operator method. In C++, this is known as violating the "Rule of Three" (or "Rule of Five" in modern C++): if a class manages a resource (like dynamically allocated memory via a pointer), it should define a destructor, copy constructor, and copy assignment operator. Without proper copy control, copying objects leads to shallow copies where multiple objects share the same pointer, causing double-free errors, use-after-free vulnerabilities, and memory corruption.

Risk

Missing copy control for pointer-containing objects has serious security implications. Shallow copies lead to double-free vulnerabilities when both objects are destroyed. Use-after-free conditions occur when one copy is deleted and the other continues to use the pointer. Memory corruption from these issues can be exploited for code execution. Data leaks may occur if sensitive data is not properly managed during copies. Resource exhaustion can happen if resources are not properly released. The undefined behavior from improper copying is unpredictable and potentially exploitable.

Solution

Follow the Rule of Three (C++03) or Rule of Five (C++11): if you define one of destructor, copy constructor, or copy assignment operator, define all of them. In modern C++, also consider move constructor and move assignment operator. Use smart pointers (unique_ptr, shared_ptr) to automate resource management. Apply the Rule of Zero: prefer using RAII wrappers so that special members are not needed. Use static analysis tools to detect violations. Consider making classes non-copyable if copying doesn't make sense. Use = delete to explicitly prevent copying when appropriate.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Memory Corruption - Double-free and use-after-free from improper copies.
AvailabilityScope: Availability

DoS: Crash - Memory corruption leads to crashes.
ConfidentialityScope: Confidentiality

Read Memory - Use-after-free may expose sensitive data.

Example Code

Vulnerable Code

// Vulnerable: Rule of Three violation
class VulnerableBuffer {
private:
    char* data;
    size_t size;

public:
    VulnerableBuffer(size_t sz) : size(sz) {
        data = new char[size];
        memset(data, 0, size);
    }

    ~VulnerableBuffer() {
        delete[] data;  // Proper cleanup
    }

    // Missing: Copy constructor!
    // Missing: Copy assignment operator!

    void write(const char* input, size_t len) {
        if (len <= size) {
            memcpy(data, input, len);
        }
    }

    char* getData() { return data; }
};

void vulnerableUsage() {
    VulnerableBuffer buf1(100);
    buf1.write("secret data", 11);

    // Vulnerable: Shallow copy - both point to same memory!
    VulnerableBuffer buf2 = buf1;  // Uses default copy constructor

    // buf1.data and buf2.data point to same memory!

    // When buf2 is destroyed, data is freed
    // When buf1 is destroyed, data is freed AGAIN (double-free!)
}

void vulnerableAssignment() {
    VulnerableBuffer buf1(100);
    VulnerableBuffer buf2(50);

    buf2 = buf1;  // Default assignment - shallow copy!
    // buf2's original memory leaked!
    // buf1 and buf2 share same pointer!
}
// Vulnerable: Class with pointer to complex object
class VulnerableDocument {
private:
    std::string* content;
    std::string* metadata;
    User* owner;  // Raw pointer to external object

public:
    VulnerableDocument(const std::string& text, User* user) {
        content = new std::string(text);
        metadata = new std::string("{}");
        owner = user;
    }

    ~VulnerableDocument() {
        delete content;
        delete metadata;
        // Don't delete owner - it's shared
    }

    // No copy constructor defined!
    // No copy assignment defined!

    void setContent(const std::string& text) {
        *content = text;
    }
};

void vulnerableDocumentCopy() {
    User user("admin");
    VulnerableDocument doc1("secret", &user);

    // Shallow copy - both docs point to same content!
    VulnerableDocument doc2 = doc1;

    doc2.setContent("modified");
    // BUG: doc1's content also changed!

    // When function exits:
    // doc2 destroyed - deletes content and metadata
    // doc1 destroyed - double-free!
}
// Vulnerable: Vector of objects with pointers
class VulnerableResource {
private:
    int* values;
    int count;

public:
    VulnerableResource(int n) : count(n) {
        values = new int[n];
    }

    ~VulnerableResource() {
        delete[] values;
    }
    // Missing copy control!
};

void vulnerableVector() {
    std::vector<VulnerableResource> resources;

    VulnerableResource r(10);
    resources.push_back(r);  // Shallow copy into vector!

    // r and resources[0] share same values pointer

    // When vector reallocates or r goes out of scope:
    // Double-free or use-after-free!
}

Fixed Code

// Fixed: Proper Rule of Three implementation
class FixedBuffer {
private:
    char* data;
    size_t size;

public:
    // Constructor
    explicit FixedBuffer(size_t sz) : size(sz), data(nullptr) {
        if (size > 0) {
            data = new char[size];
            memset(data, 0, size);
        }
    }

    // Destructor
    ~FixedBuffer() {
        delete[] data;
    }

    // Copy constructor - deep copy
    FixedBuffer(const FixedBuffer& other) : size(other.size), data(nullptr) {
        if (size > 0) {
            data = new char[size];
            memcpy(data, other.data, size);
        }
    }

    // Copy assignment operator - deep copy with proper cleanup
    FixedBuffer& operator=(const FixedBuffer& other) {
        if (this != &other) {  // Self-assignment check
            // Create new buffer first (exception safety)
            char* newData = nullptr;
            if (other.size > 0) {
                newData = new char[other.size];
                memcpy(newData, other.data, other.size);
            }

            // Clean up old data
            delete[] data;

            // Assign new data
            data = newData;
            size = other.size;
        }
        return *this;
    }

    void write(const char* input, size_t len) {
        if (data && len <= size) {
            memcpy(data, input, len);
        }
    }

    const char* getData() const { return data; }
    size_t getSize() const { return size; }
};
// Fixed: Rule of Five (C++11) with move semantics
class ModernBuffer {
private:
    char* data;
    size_t size;

public:
    // Constructor
    explicit ModernBuffer(size_t sz) : size(sz), data(nullptr) {
        if (size > 0) {
            data = new char[size]();  // Value-initialized
        }
    }

    // Destructor
    ~ModernBuffer() {
        delete[] data;
    }

    // Copy constructor
    ModernBuffer(const ModernBuffer& other) : size(other.size), data(nullptr) {
        if (size > 0) {
            data = new char[size];
            std::copy(other.data, other.data + size, data);
        }
    }

    // Move constructor
    ModernBuffer(ModernBuffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }

    // Copy assignment
    ModernBuffer& operator=(const ModernBuffer& other) {
        if (this != &other) {
            ModernBuffer temp(other);  // Copy-and-swap idiom
            swap(*this, temp);
        }
        return *this;
    }

    // Move assignment
    ModernBuffer& operator=(ModernBuffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }

    // Swap function for copy-and-swap
    friend void swap(ModernBuffer& a, ModernBuffer& b) noexcept {
        using std::swap;
        swap(a.data, b.data);
        swap(a.size, b.size);
    }

    // Accessors
    const char* getData() const { return data; }
    size_t getSize() const { return size; }
};
// Fixed: Rule of Zero - use smart pointers
class SmartBuffer {
private:
    std::unique_ptr<char[]> data;
    size_t size;

public:
    explicit SmartBuffer(size_t sz) : size(sz) {
        if (size > 0) {
            data = std::make_unique<char[]>(size);
        }
    }

    // No destructor needed - unique_ptr handles cleanup
    // No copy constructor needed - unique_ptr is move-only

    // If copying is needed, implement deep copy:
    SmartBuffer(const SmartBuffer& other) : size(other.size) {
        if (size > 0) {
            data = std::make_unique<char[]>(size);
            std::copy(other.data.get(), other.data.get() + size, data.get());
        }
    }

    SmartBuffer& operator=(const SmartBuffer& other) {
        if (this != &other) {
            size = other.size;
            if (size > 0) {
                data = std::make_unique<char[]>(size);
                std::copy(other.data.get(), other.data.get() + size, data.get());
            } else {
                data.reset();
            }
        }
        return *this;
    }

    // Move operations are automatically generated for unique_ptr
    SmartBuffer(SmartBuffer&&) = default;
    SmartBuffer& operator=(SmartBuffer&&) = default;

    const char* getData() const { return data.get(); }
};
// Fixed: Non-copyable class when copying doesn't make sense
class NonCopyableResource {
private:
    int* handle;

public:
    explicit NonCopyableResource(int id) {
        handle = acquireResource(id);
    }

    ~NonCopyableResource() {
        releaseResource(handle);
    }

    // Explicitly delete copy operations
    NonCopyableResource(const NonCopyableResource&) = delete;
    NonCopyableResource& operator=(const NonCopyableResource&) = delete;

    // Allow move operations
    NonCopyableResource(NonCopyableResource&& other) noexcept
        : handle(other.handle) {
        other.handle = nullptr;
    }

    NonCopyableResource& operator=(NonCopyableResource&& other) noexcept {
        if (this != &other) {
            releaseResource(handle);
            handle = other.handle;
            other.handle = nullptr;
        }
        return *this;
    }

    int* getHandle() const { return handle; }
};

// Safe usage in vector with move semantics
void safeVector() {
    std::vector<NonCopyableResource> resources;

    resources.push_back(NonCopyableResource(1));  // Move into vector
    resources.emplace_back(2);  // Construct in place

    // No double-free - each resource has unique ownership
}
// Fixed: Document with proper copy semantics
class FixedDocument {
private:
    std::unique_ptr<std::string> content;
    std::unique_ptr<std::string> metadata;
    std::shared_ptr<User> owner;  // Shared ownership

public:
    FixedDocument(const std::string& text, std::shared_ptr<User> user)
        : content(std::make_unique<std::string>(text)),
          metadata(std::make_unique<std::string>("{}")),
          owner(std::move(user)) {}

    // Deep copy
    FixedDocument(const FixedDocument& other)
        : content(std::make_unique<std::string>(*other.content)),
          metadata(std::make_unique<std::string>(*other.metadata)),
          owner(other.owner) {}  // Shared pointer - shared ownership

    FixedDocument& operator=(const FixedDocument& other) {
        if (this != &other) {
            content = std::make_unique<std::string>(*other.content);
            metadata = std::make_unique<std::string>(*other.metadata);
            owner = other.owner;
        }
        return *this;
    }

    // Default move operations work correctly with smart pointers
    FixedDocument(FixedDocument&&) = default;
    FixedDocument& operator=(FixedDocument&&) = default;

    void setContent(const std::string& text) {
        *content = text;
    }

    const std::string& getContent() const { return *content; }
};

void safeDocumentCopy() {
    auto user = std::make_shared<User>("admin");
    FixedDocument doc1("secret", user);

    // Deep copy - independent content
    FixedDocument doc2 = doc1;

    doc2.setContent("modified");
    // doc1's content unchanged - "secret"
    // doc2's content is "modified"

    // Both share the same User through shared_ptr
    // No double-free issues
}

CVE Examples

Double-free and use-after-free vulnerabilities from missing copy control have been common sources of exploitable memory corruption bugs in C++ applications.


  • CWE-1076: Insufficient Adherence to Expected Conventions (parent)
  • CWE-415: Double Free (can result from)
  • CWE-416: Use After Free (can result from)
  • CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer (related)

References

  1. MITRE Corporation. "CWE-1098: Data Element containing Pointer Item without Proper Copy Control Element." https://cwe.mitre.org/data/definitions/1098.html
  2. Stroustrup, Bjarne. "The C++ Programming Language" - Rule of Three/Five/Zero.
  3. C++ Core Guidelines. C.21: If you define or =delete any default operation, define or =delete them all.