Skip to content

💀 Phụ Lục A: Sai Lầm Bộ Nhớ Mà Beginner Mang Vào Production Appendix

Tại sao phụ lục này tồn tại?

⚠️ Cảnh báo nghiêm túc

Những lỗi trong phụ lục này không phải lý thuyết. Đây là những bug thực tế gây ra crash, lỗ hổng bảo mật, và corruption dữ liệu trên hệ thống production — ảnh hưởng hàng triệu người dùng.

Có một hiện tượng rất phổ biến trong giới lập trình C++: "Works on my machine". Code chạy ngon trên máy dev, pass hết unit test, nhưng khi lên production — dưới tải thực, timing khác, compiler khác — mọi thứ nổ tung.

Mỗi lỗi trong phụ lục này bao gồm:

Thành phầnMô tả
🐛 Code mẫuCode buggy — chính xác như beginner viết
🔍 Root causeTại sao bug xảy ra ở tầng memory
💥 Hậu quả productionChuyện gì xảy ra khi bug này lên production
Fix bằng modern C++Cách sửa đúng theo C++17/20
🛠️ Tool phát hiệnTool nào bắt được bug này

📖 Tiên quyết

Bạn nên đọc RAII & Constructors trước khi đọc phụ lục này. Phần lớn các fix đều dựa trên nguyên tắc RAII.


Lỗi #1: Memory Leak — new không có delete

🐛 Code buggy

cpp
// ❌ BUG: Memory leak trên mỗi request
void processRequest(const std::string& input) {
    auto* data = new RequestData(parse(input));
    handleRequest(data);
    // Quên delete! Leak trên MỖI request.
    // Nếu handleRequest() throw exception → leak chắc chắn.
}

🔍 Root cause

Mỗi lần gọi new, hệ điều hành cấp phát một block trên heap. Block này sẽ tồn tại mãi mãi cho đến khi được giải phóng bằng delete. Khi function kết thúc (bình thường hoặc do exception), pointer data trên stack bị hủy — nhưng block heap vẫn còn đó, không ai giữ địa chỉ của nó nữa.

Stack (tự dọn)          Heap (phải dọn thủ công)
┌──────────────┐         ┌─────────────────┐
│ data = 0xABC │───────► │  RequestData    │
└──────────────┘         │  (100 bytes)    │
   ↑ bị hủy khi          │  LEAKED! 💀     │
   function return        └─────────────────┘
                            ↑ không ai delete

💥 Hậu quả production

  • Server web xử lý 1000 request/giây, mỗi request leak 1KB → 1MB/giây3.6GB/giờ
  • Sau vài giờ: hệ điều hành gọi OOM Killer → process bị kill không thương tiếc
  • Khách hàng thấy: 502 Bad Gateway — không có error message, không có stack trace
  • DevOps team: "Server lại crash rồi, restart đi" → triệu chứng lặp lại

Fix bằng modern C++

cpp
// Tự động delete khi ra khỏi scope — kể cả khi exception
void processRequest(const std::string& input) {
    auto data = std::make_unique<RequestData>(parse(input));
    handleRequest(data.get());
    // data tự động bị delete ở đây — RAII đảm bảo
}
cpp
// Không cần heap allocation — nhanh hơn, an toàn hơn
void processRequest(const std::string& input) {
    RequestData data(parse(input));
    handleRequest(&data);
    // data tự động hủy trên stack — zero overhead
}

🛠️ Tool phát hiện

ToolLệnh
LeakSanitizerg++ -fsanitize=leak -o prog prog.cpp && ./prog
Valgrindvalgrind --leak-check=full ./prog

💡 Nguyên tắc

Nếu bạn viết new mà không nằm trong make_unique hoặc make_shared — hãy dừng lại và hỏi tại sao.


Lỗi #2: Use-After-Free — Trả về địa chỉ biến local

🐛 Code buggy

cpp
// ❌ BUG: Trả về pointer đến biến đã bị hủy
int* getData() {
    int local = 42;
    return &local;  // local bị hủy khi function return!
}

void caller() {
    int* ptr = getData();
    std::cout << *ptr;  // Undefined Behavior!
    // Có thể in 42, có thể in rác, có thể crash
}

🔍 Root cause

Biến local sống trên stack frame của getData(). Khi function return, stack frame bị thu hồi. Pointer trả về vẫn trỏ đến vùng nhớ đó — nhưng vùng nhớ đó giờ là "đất vô chủ": có thể bị ghi đè bởi bất kỳ function nào gọi tiếp theo.

Trước return:              Sau return:
┌──────────────┐           ┌──────────────┐
│ getData()    │           │ ???          │ ← stack frame đã bị thu hồi
│ local = 42   │           │ rác / data   │
│ addr: 0xFF00 │           │ của func khác│
└──────────────┘           └──────────────┘
       ↑                          ↑
   ptr trỏ đây              ptr VẪN trỏ đây → 💥 UB

💥 Hậu quả production

  • Hành vi không xác định (UB): Có thể "chạy đúng" trong debug mode, crash trong release mode
  • Lỗ hổng bảo mật cấp CVE: Attacker có thể đọc dữ liệu nhạy cảm từ stack (passwords, tokens)
  • Heisenbug: Bug biến mất khi bạn thêm printf để debug (vì printf thay đổi layout stack)
  • Đây là loại bug đứng sau hàng ngàn CVE trong lịch sử phần mềm

Fix bằng modern C++

cpp
// Copy elision (C++17 guaranteed) → zero-cost
int getData() {
    int local = 42;
    return local;  // Trả về VALUE, không phải address
}
cpp
std::unique_ptr<LargeObject> getData() {
    auto obj = std::make_unique<LargeObject>();
    obj->initialize();
    return obj;  // Move semantics — zero-copy
}
cpp
void getData(int& result) {
    result = 42;  // Ghi trực tiếp vào biến của caller
}

🛠️ Tool phát hiện

ToolLệnh
AddressSanitizerg++ -fsanitize=address -o prog prog.cpp
Compiler warningg++ -Wall -Wextra -Wreturn-local-addr

Lỗi #3: Double Free — Xóa cùng một vùng nhớ hai lần

🐛 Code buggy

cpp
// ❌ BUG: Double free
void processData() {
    int* ptr = new int(42);
    doSomething(ptr);
    delete ptr;

    // ... 200 dòng code sau ...

    cleanup(ptr);  // Hàm cleanup cũng gọi delete ptr!
}

void cleanup(int* p) {
    delete p;  // 💥 Double free — heap corruption
}

🔍 Root cause

Lần delete đầu tiên đánh dấu block nhớ là "available" trong heap allocator. Lần delete thứ hai cố giải phóng block đã được đánh dấu free — phá hỏng metadata của heap allocator.

Hậu quả: heap allocator có thể cấp phát cùng block đó cho hai object khác nhau → silent data corruption.

Sau delete lần 1:           Sau delete lần 2:
┌─────────┐                 ┌─────────┐
│  FREE   │  ← hợp lệ      │ CORRUPT │ ← heap metadata bị phá
│ (sẵn    │                 │ free-   │
│  sàng   │                 │ list    │
│  cấp    │                 │ hỏng!   │
│  phát)  │                 │  💀💀💀  │
└─────────┘                 └─────────┘

💥 Hậu quả production

  • Heap corruption → crash ngẫu nhiên tại thời điểm KHÔNG liên quan (rất khó debug)
  • Lỗ hổng bảo mật: Attacker có thể khai thác double-free để thực thi code tùy ý
  • Cùng class lỗi với CVE-2021-21224 (Chrome V8 engine)
  • Crash report sẽ chỉ vào chỗ KHÔNG phải nguyên nhân → debug mất hàng ngày

Fix bằng modern C++

cpp
void processData() {
    auto ptr = std::make_unique<int>(42);
    doSomething(ptr.get());
    // ptr tự động delete DUY NHẤT MỘT LẦN khi ra scope
    // Không cần cleanup() — RAII lo hết
}
cpp
void processData() {
    int* ptr = new int(42);
    doSomething(ptr);
    delete ptr;
    ptr = nullptr;  // delete nullptr là no-op (an toàn)
    // Nhưng ĐỪNG dùng cách này — dùng unique_ptr đi
}

🛠️ Tool phát hiện

ToolLệnh
AddressSanitizerg++ -fsanitize=address -o prog prog.cpp
Valgrindvalgrind ./prog

Lỗi #4: Buffer Overflow — Ghi ngoài mảng

🐛 Code buggy

cpp
// ❌ BUG: Off-by-one → buffer overflow
void fillArray() {
    int arr[10];
    for (int i = 0; i <= 10; i++) {  // i=10 → OUT OF BOUNDS!
        arr[i] = i * 2;
    }
    // arr[10] ghi đè lên stack — có thể ghi đè return address
}
cpp
// ❌ BUG: Không kiểm tra kích thước input
void copyInput(const char* userInput) {
    char buffer[64];
    strcpy(buffer, userInput);  // userInput dài 200 ký tự → 💥
}

🔍 Root cause

Mảng C-style (int arr[10]) không kiểm tra bounds. Khi truy cập arr[10], compiler không báo lỗi — nó cứ ghi vào vùng nhớ tiếp theo trên stack. Vùng nhớ đó có thể là:

  • Biến local khác → silent data corruption
  • Stack frame metadata → crash
  • Return address → attacker kiểm soát luồng thực thi → Remote Code Execution (RCE)
Stack layout:
┌────────────┐ ← địa chỉ cao
│ return addr│ ← nếu bị ghi đè → attacker kiểm soát
├────────────┤
│ saved EBP  │
├────────────┤
│ arr[9]     │
│ arr[8]     │
│ ...        │
│ arr[0]     │ ← địa chỉ thấp
└────────────┘
   arr[10] ghi vào ĐÂY ↗ (tràn lên saved EBP hoặc return addr)

💥 Hậu quả production

  • Stack smashing → process crash với *** stack smashing detected ***
  • Remote Code Execution: Đây là kỹ thuật tấn công #1 trong lịch sử — từ Morris Worm (1988) đến ngày nay
  • Heartbleed (CVE-2014-0160): Buffer over-read trong OpenSSL → lộ private keys, passwords, tokens của hàng triệu server
  • Chi phí sửa Heartbleed ước tính: hàng trăm triệu USD

Fix bằng modern C++

cpp
void fillArray() {
    std::vector<int> arr(10);
    for (int i = 0; i < static_cast<int>(arr.size()); i++) {
        arr[i] = i * 2;
    }
    // Hoặc đẹp hơn:
    for (auto& val : arr) {
        val = /* ... */;
    }
}
cpp
void fillArray() {
    std::array<int, 10> arr{};
    for (int i = 0; i < static_cast<int>(arr.size()); i++) {
        arr.at(i) = i * 2;  // .at() throw std::out_of_range nếu ngoài bounds
    }
}
cpp
void copyInput(const std::string& userInput) {
    // std::string tự quản lý bộ nhớ — không thể overflow
    std::string buffer = userInput;
    // Hoặc nếu cần giới hạn:
    std::string buffer = userInput.substr(0, 64);
}

🛠️ Tool phát hiện

ToolLệnh
AddressSanitizerg++ -fsanitize=address -o prog prog.cpp
Stack Protectorg++ -fstack-protector-strong (bật mặc định trên hầu hết compiler)
Fortify Sourceg++ -D_FORTIFY_SOURCE=2

Lỗi #5: Dangling Pointer sau Vector Resize

🐛 Code buggy

cpp
// ❌ BUG: Dangling pointer sau vector reallocation
void processBatch() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    int* firstElement = &data[0];  // Lưu raw pointer vào vector

    // Thêm elements → vector CÓ THỂ reallocation (di chuyển toàn bộ data)
    for (int i = 6; i <= 100; i++) {
        data.push_back(i);
    }

    // firstElement giờ trỏ vào vùng nhớ CŨ đã bị free!
    std::cout << *firstElement;  // 💥 Use-after-free
}

🔍 Root cause

std::vector lưu data trong một block liên tục trên heap. Khi push_back() vượt quá capacity(), vector phải:

  1. Cấp phát block MỚI lớn hơn (thường gấp đôi)
  2. Copy/move tất cả element sang block mới
  3. Free block cũ ← pointer cũ trở thành dangling!
Trước reallocation:          Sau reallocation:
data: [1,2,3,4,5,_,_,_]     data: [1,2,3,4,...,100,_,_,_,...] (block MỚI)

  firstElement                Block CŨ đã bị FREE 💀
                              firstElement vẫn trỏ vào block cũ → 💥

💥 Hậu quả production

  • Intermittent crash: Chỉ crash khi vector vượt capacity → khó tái tạo
  • Data corruption: đọc/ghi vào vùng nhớ đã bị cấp phát cho object khác
  • Bug chỉ xuất hiện khi data đủ lớn → test nhỏ pass, production fail

Fix bằng modern C++

cpp
void processBatch() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    size_t firstIndex = 0;  // Index không bao giờ invalidate

    for (int i = 6; i <= 100; i++) {
        data.push_back(i);
    }

    std::cout << data[firstIndex];  // Luôn hợp lệ
}
cpp
void processBatch() {
    std::vector<int> data;
    data.reserve(100);  // Cấp phát đủ trước → không reallocation
    int* firstElement = &data[0];  // An toàn nếu không vượt reserve

    for (int i = 1; i <= 100; i++) {
        data.push_back(i);
    }
    // firstElement vẫn valid vì không có reallocation
}

🛠️ Tool phát hiện

ToolLệnh
AddressSanitizerg++ -fsanitize=address -o prog prog.cpp
libstdc++ debug modeg++ -D_GLIBCXX_DEBUG -o prog prog.cpp

💡 Quy tắc vàng

Không bao giờ giữ raw pointer hoặc reference đến element của std::vector qua bất kỳ thao tác nào có thể gây reallocation (push_back, insert, resize, emplace_back).


Lỗi #6: Quên Virtual Destructor — Leak trong Polymorphism

🐛 Code buggy

cpp
// ❌ BUG: Base class không có virtual destructor
class Logger {
public:
    Logger() { std::cout << "Logger created\n"; }
    ~Logger() { std::cout << "Logger destroyed\n"; }  // KHÔNG virtual!
};

class FileLogger : public Logger {
    std::FILE* file_;
public:
    FileLogger(const char* path) : file_(std::fopen(path, "w")) {}
    ~FileLogger() {
        if (file_) std::fclose(file_);  // Đóng file handle
        std::cout << "FileLogger destroyed\n";
    }
};

void createLogger() {
    Logger* logger = new FileLogger("/var/log/app.log");
    // ... sử dụng logger ...
    delete logger;  // 💥 Chỉ gọi Logger::~Logger()!
    // FileLogger::~FileLogger() KHÔNG được gọi
    // → file handle LEAKED
}

🔍 Root cause

Khi delete được gọi qua pointer kiểu Base*, compiler chỉ gọi destructor của Base — trừ khi destructor là virtual. Không có virtual, compiler không biết object thực sự là Derived.

Với ~Logger() KHÔNG virtual:
delete logger → Logger::~Logger() ✓
               FileLogger::~FileLogger() ✗ ← KHÔNG gọi!
               → file_ không được fclose → RESOURCE LEAK

Với virtual ~Logger():
delete logger → FileLogger::~FileLogger() ✓ (qua vtable)
               Logger::~Logger() ✓ (chain up tự động)
               → tất cả resource được dọn dẹp

💥 Hậu quả production

  • Resource leak: File handle, socket, database connection không được đóng
  • Hệ điều hành có giới hạn file descriptor (thường 1024) → "Too many open files"
  • Server từ chối connection mới → denial of service
  • Memory leak nếu derived class có member được new

Fix bằng modern C++

cpp
class Logger {
public:
    Logger() = default;
    virtual ~Logger() = default;  // BẮT BUỘC khi có inheritance
};

class FileLogger : public Logger {
    std::unique_ptr<std::FILE, decltype(&std::fclose)> file_;
public:
    FileLogger(const char* path)
        : file_(std::fopen(path, "w"), &std::fclose) {}
    // ~FileLogger tự động — unique_ptr lo việc fclose
};
cpp
void createLogger() {
    // unique_ptr + virtual destructor = an toàn tuyệt đối
    auto logger = std::make_unique<FileLogger>("/var/log/app.log");
    // Tự động gọi đúng destructor khi ra scope
}

🛠️ Tool phát hiện

ToolLệnh
Clang-Tidyclang-tidy -checks='cppcoreguidelines-virtual-class-destructor'
Compiler warningg++ -Wnon-virtual-dtor

📝 Quy tắc C++ Core Guidelines

C.35: Destructor của base class phải là publicvirtual, hoặc protected và non-virtual.

Nói đơn giản: nếu class có thể bị inheritbị delete qua base pointer → destructor phải virtual.


Lỗi #7: Đọc Biến Chưa Khởi Tạo

🐛 Code buggy

cpp
// ❌ BUG: Đọc biến chưa khởi tạo
int calculateScore(bool bonusEligible) {
    int score;       // Chưa khởi tạo — giá trị là RÁC
    int bonus;       // Cũng rác

    if (bonusEligible) {
        score = 100;
        bonus = 50;
    }

    // Nếu bonusEligible == false:
    // score và bonus chứa giá trị RÁC từ stack
    return score + bonus;  // 💥 Undefined Behavior
}

🔍 Root cause

Trong C++, biến local không tự khởi tạo (khác với Java, Python, C#). Giá trị của biến chưa khởi tạo là bất kỳ byte nào đang nằm ở vị trí đó trên stack — có thể là 0, có thể là 0xDEADBEEF, có thể là password từ function trước đó.

Nghiêm trọng hơn: Theo tiêu chuẩn C++, đọc biến chưa khởi tạo là Undefined Behavior. Compiler được phép làm BẤT CỨ ĐIỀU GÌ — bao gồm tối ưu hóa bỏ cả if statement.

💥 Hậu quả production

  • Kết quả sai ngẫu nhiên: Score có thể là 42, -7893241, hoặc 0 — tùy lần chạy
  • Branch prediction: Compiler có thể optimize dựa trên giả định "biến đã được khởi tạo" → logic sai hoàn toàn
  • Security: Nếu biến chưa khởi tạo chứa sensitive data từ function trước → information leak
  • Flaky tests: Test pass trên CI, fail trên local (hoặc ngược lại) vì stack layout khác nhau

Fix bằng modern C++

cpp
// ✅ FIX: Luôn khởi tạo — ngay cả khi "chắc chắn" sẽ gán sau
int calculateScore(bool bonusEligible) {
    int score = 0;    // Khởi tạo mặc định
    int bonus = 0;

    if (bonusEligible) {
        score = 100;
        bonus = 50;
    }

    return score + bonus;  // Luôn xác định: 0 hoặc 150
}
cpp
// ✅ BETTER: Dùng constexpr/const khi có thể
int calculateScore(bool bonusEligible) {
    const int score = bonusEligible ? 100 : 0;
    const int bonus = bonusEligible ? 50 : 0;
    return score + bonus;
}

🛠️ Tool phát hiện

ToolLệnh
Compiler warningsg++ -Wall -Wextra -Wuninitialized -Werror
MemorySanitizerclang++ -fsanitize=memory -o prog prog.cpp
Clang-Tidyclang-tidy -checks='cppcoreguidelines-init-variables'

💡 Mẹo thực hành

Luôn compile với -Wall -Wextra -Werror trong development. Biến mọi warning thành error — đừng để warning tích lũy.


Lỗi #8: Stack Overflow từ Đệ Quy Không Giới Hạn

🐛 Code buggy

cpp
// ❌ BUG: Không có base case cho nullptr
struct Node {
    int value;
    Node* left;
    Node* right;
};

int countNodes(Node* node) {
    // Quên kiểm tra nullptr!
    return 1 + countNodes(node->left) + countNodes(node->right);
    // Khi node->left == nullptr → truy cập nullptr->left → 💥 crash
    // Hoặc: đệ quy vô hạn nếu tree có cycle
}
cpp
// ❌ BUG: Đệ quy quá sâu trên input lớn
long long fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
    // fibonacci(50) → ~2^50 lần gọi → stack overflow
    // (và chậm kinh khủng — O(2^n))
}

🔍 Root cause

Mỗi lần gọi function, một stack frame mới được push lên call stack. Stack có kích thước giới hạn (thường 1-8 MB tùy OS). Đệ quy không giới hạn sẽ dùng hết stack → stack overflow → crash không thể recover.

Call stack khi tree sâu 100,000 nodes:
┌────────────────────┐
│ countNodes(leaf)    │  ← frame #100,000
├────────────────────┤
│ countNodes(...)     │  ← frame #99,999
├────────────────────┤
│ ...                 │  ← 99,997 frames nữa
├────────────────────┤
│ countNodes(root)    │  ← frame #1
├────────────────────┤
│ main()              │
└────────────────────┘
   ↕ Mỗi frame ~100 bytes → 100,000 × 100 = 10 MB > stack limit → 💥

💥 Hậu quả production

  • Segmentation fault — process crash ngay lập tức
  • Không thể catch: Stack overflow KHÔNG phải exception — try/catch không giúp được
  • Trên Linux: nhận signal SIGSEGV → core dump (nếu được bật)
  • Input nhỏ → test pass; input lớn → production crash → "works on my machine" kinh điển

Fix bằng modern C++

cpp
int countNodes(Node* node) {
    if (node == nullptr) return 0;  // BASE CASE!
    return 1 + countNodes(node->left) + countNodes(node->right);
}
cpp
// Dùng explicit stack thay vì call stack
int countNodes(Node* root) {
    if (root == nullptr) return 0;

    int count = 0;
    std::stack<Node*> stk;
    stk.push(root);

    while (!stk.empty()) {
        Node* node = stk.top();
        stk.pop();
        count++;

        if (node->left) stk.push(node->left);
        if (node->right) stk.push(node->right);
    }
    return count;
}
cpp
long long fibonacci(int n) {
    if (n <= 1) return n;
    long long prev2 = 0, prev1 = 1;
    for (int i = 2; i <= n; i++) {
        long long curr = prev1 + prev2;
        prev2 = prev1;
        prev1 = curr;
    }
    return prev1;
}

🛠️ Tool phát hiện

ToolLệnh
UBSanitizerg++ -fsanitize=undefined -o prog prog.cpp
Stack size checkulimit -s (Linux) — xem stack limit

Lỗi #9 (Bonus): new[] / delete Không Khớp

🐛 Code buggy

cpp
// ❌ BUG: Dùng delete thay vì delete[]
void processImage() {
    int* pixels = new int[1920 * 1080];  // Cấp phát MẢNG

    // ... xử lý ảnh ...

    delete pixels;    // 💥 SAI! Phải dùng delete[]
    // delete chỉ hủy element ĐẦU TIÊN
    // Các element còn lại: destructor không được gọi + heap metadata sai
}

🔍 Root cause

new[]delete[] là hai operator hoàn toàn khác so với newdelete:

  • new[] cấp phát mảng + lưu kích thước mảng trong metadata
  • delete[] đọc metadata đó để biết phải gọi bao nhiêu destructor
  • delete (không có []) không đọc metadata mảng → chỉ gọi 1 destructor → heap corruption

💥 Hậu quả production

  • Undefined Behavior theo tiêu chuẩn C++
  • Trên hầu hết implementation: heap corruption → crash ngẫu nhiên sau đó
  • Nếu element type có destructor (ví dụ std::string): chỉ 1 string được hủy, hàng triệu string leaked
  • Bug RẤT khó phát hiện bằng mắt — code "trông đúng" nhưng sai

Fix bằng modern C++

cpp
// ✅ FIX: Dùng std::vector — không bao giờ sai
void processImage() {
    std::vector<int> pixels(1920 * 1080);
    // ... xử lý ảnh ...
    // Tự động dọn dẹp — đúng cách, mọi lúc
}
cpp
// ✅ Nếu cần fixed-size array:
#include <array>
void processSmallData() {
    std::array<int, 256> buffer{};
    // Stack allocation, zero overhead, type-safe
}

🛠️ Tool phát hiện

ToolLệnh
AddressSanitizerg++ -fsanitize=address -o prog prog.cpp
Clang-Tidyclang-tidy -checks='clang-analyzer-cplusplus.NewDeleteLeaks'

🛠️ Tool Arsenal — Bộ Vũ Khí Phát Hiện Bug Bộ Nhớ

📋 Bảng tổng hợp tool

Không có tool nào bắt được tất cả bug. Dùng nhiều tool kết hợp để tối đa coverage.

ToolBắt được gìCách sử dụngOverhead
AddressSanitizer (ASan)Use-after-free, buffer overflow, double-free, stack overflowg++ -fsanitize=address~2x chậm hơn
MemorySanitizer (MSan)Đọc biến chưa khởi tạoclang++ -fsanitize=memory~3x chậm hơn
LeakSanitizer (LSan)Memory leak-fsanitize=leak (bật mặc định với ASan)Nhẹ
UBSanitizer (UBSan)Undefined behavior (overflow, null deref, etc.)-fsanitize=undefined~1.2x chậm hơn
ValgrindLeak, uninitialized read, invalid accessvalgrind --leak-check=full ./prog~20x chậm hơn
Clang-TidyStatic analysis — phát hiện lúc compileclang-tidy -checks='*' file.cpp0 (compile-time)
Clang Static AnalyzerDeep path-sensitive analysisscan-build g++ -o prog prog.cpp0 (compile-time)

Cấu hình khuyến nghị cho CI/CD

bash
# Trong Makefile hoặc CMakeLists.txt — build mode "sanitize"
CXX_FLAGS_SANITIZE = -fsanitize=address,undefined -fno-omit-frame-pointer -g
CXX_FLAGS_RELEASE  = -O2 -DNDEBUG

# CI pipeline: chạy test với sanitizer BẬT
# Production build: sanitizer TẮT (vì overhead)
cmake
# CMakeLists.txt
option(ENABLE_SANITIZERS "Enable ASan + UBSan" OFF)

if(ENABLE_SANITIZERS)
    add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
    add_link_options(-fsanitize=address,undefined)
endif()

🔑 Best practice

  • Development: Bật ASan + UBSan → bắt bug sớm nhất
  • CI/CD: Chạy full test suite với sanitizer
  • Staging: Valgrind trên critical path
  • Production: Không bật sanitizer (overhead), nhưng dùng -fstack-protector-strong

🎯 Tổng Kết: Modern C++ Solution Map

Bảng dưới đây là cheat sheet — mỗi khi bạn cần dùng bộ nhớ, tra bảng này:

Thay vì...Dùng...Lý do
new T(...)std::make_unique<T>(...)Tự động delete, exception-safe
new T[n]std::vector<T>(n)Tự động quản lý, bounds-check với .at()
delete / delete[](không cần)Smart pointer / container lo
Raw pointer để own resourcestd::unique_ptr<T>Ownership rõ ràng, zero overhead
Raw pointer chia sẻ resourcestd::shared_ptr<T>Reference counting tự động
malloc / freestd::vector hoặc std::make_uniqueType-safe, RAII
C-style array int arr[N]std::array<int, N>Bounds-check, pass by value, .size()
char buf[N] + strcpystd::stringTự quản lý bộ nhớ, không overflow
Manual resource managementRAII (constructor acquire, destructor release)Nguyên tắc nền tảng

5 quy tắc vàng

📋 Ghi nhớ

  1. Không new/delete trực tiếp — dùng make_unique/make_shared
  2. Không raw array — dùng std::vector hoặc std::array
  3. Không raw pointer sở hữu resource — dùng smart pointer
  4. RAII cho mọi resource — file, socket, mutex, memory
  5. Rule of 0: Nếu dùng đúng smart pointer và container, bạn không cần viết destructor

💀 Production War Story: Heartbleed

🔴 Câu chuyện thật: CVE-2014-0160 — Heartbleed

Năm 2014, một buffer over-read trong OpenSSL đã trở thành lỗ hổng bảo mật nghiêm trọng nhất trong lịch sử internet.

Chuyện gì xảy ra?

Code trong OpenSSL xử lý gói "heartbeat" của TLS:

c
// Simplified version of the bug
// Client gửi: "echo back this payload, length = 65535"
// Nhưng payload thực tế chỉ có 1 byte!
memcpy(response, payload_ptr, client_claimed_length);
// → Copy 65535 bytes từ bộ nhớ server → bao gồm:
//   - Private keys
//   - Passwords
//   - Session tokens
//   - Dữ liệu của user KHÁC

Root cause?

Không validate client_claimed_length trước khi dùng nó trong memcpy. Một lỗi buffer over-read cơ bản — chính xác loại bug mà std::vector::at() hoặc std::span sẽ ngăn chặn.

Hậu quả?

  • ~17% server HTTPS trên internet bị ảnh hưởng (~500,000 server)
  • Private key bị lộ → mọi traffic HTTPS đã encrypted đều có thể bị giải mã
  • Hàng loạt công ty phải thu hồi và phát hành lại SSL certificate
  • Chi phí ước tính: hàng trăm triệu đến hàng tỷ USD
  • Mọi user trên internet phải đổi password

Bài học?

Một dòng code thiếu bounds-checking đã khiến nửa internet phải reset security.

Nếu OpenSSL được viết bằng modern C++ với std::vector và bounds-checking — Heartbleed đã không xảy ra.


🧩 Spot-the-Bug Challenge

🐛 Challenge: Tìm TẤT CẢ bug trong đoạn code sau
cpp
class DataProcessor {
    int* buffer_;
    int size_;
public:
    DataProcessor(int n) {
        buffer_ = new int[n];
        size_ = n;
    }

    ~DataProcessor() {
        delete buffer_;  // Bug #1: ???
    }

    int* getSlice(int start, int len) {
        int* slice = new int[len];
        for (int i = 0; i <= len; i++) {  // Bug #2: ???
            slice[i] = buffer_[start + i];  // Bug #3: ???
        }
        return slice;  // Bug #4: ???
    }

    void resize(int newSize) {
        delete buffer_;
        buffer_ = new int[newSize];  // Bug #5: ???
        size_ = newSize;
    }
};
✅ Đáp án chi tiết

Bug #1: delete buffer_ — phải là delete[] buffer_ (new[] / delete mismatch)

Bug #2: i <= len — off-by-one, phải là i < len (buffer overflow)

Bug #3: Không kiểm tra start + i < size_ — có thể đọc ngoài buffer_ (buffer over-read)

Bug #4: Trả về raw new int[] mà caller không biết phải delete[] — memory leak chắc chắn

Bug #5: resize mất toàn bộ data cũ — không copy data từ buffer cũ sang buffer mới. Ngoài ra: nếu new int[newSize] throw exception sau delete buffer_, thì buffer_ trở thành dangling pointer

Bonus Bug: Class vi phạm Rule of 3/5 — không có copy constructor và copy assignment operator. Nếu copy DataProcessor, cả hai object sẽ delete[] cùng một buffer_double free.

Fix hoàn chỉnh bằng modern C++:

cpp
class DataProcessor {
    std::vector<int> buffer_;
public:
    explicit DataProcessor(int n) : buffer_(n) {}
    // Không cần destructor — Rule of 0!
    // Không cần copy/move constructor — compiler tự generate đúng!

    std::vector<int> getSlice(int start, int len) const {
        if (start < 0 || len < 0 ||
            static_cast<size_t>(start + len) > buffer_.size()) {
            throw std::out_of_range("Invalid slice range");
        }
        return {buffer_.begin() + start,
                buffer_.begin() + start + len};
    }

    void resize(int newSize) {
        buffer_.resize(newSize);  // Giữ data cũ + exception-safe
    }
};

📚 Scenario: Code Review Thực Tế

🎬 Scenario: Bạn là reviewer — đồng nghiệp gửi PR này
cpp
// file: user_cache.cpp
class UserCache {
    std::unordered_map<int, User*> cache_;
public:
    void addUser(int id, const std::string& name) {
        cache_[id] = new User(id, name);
    }

    User* getUser(int id) {
        return cache_[id];
    }

    void removeUser(int id) {
        cache_.erase(id);
    }

    ~UserCache() {
        for (auto& [id, user] : cache_) {
            delete user;
        }
    }
};

Bạn sẽ comment gì trong code review?

✅ Code review comments

Comment 1removeUser: Memory leak! erase() xóa entry khỏi map nhưng KHÔNG delete User*. Mỗi lần removeUser → leak một User object.

Comment 2getUser: Trả về raw User* — caller có thể delete nó → double free khi destructor chạy. Hoặc caller giữ pointer sau removeUser → dangling.

Comment 3addUser: Nếu cache_[id] đã tồn tại → User* cũ bị ghi đè → memory leak.

Comment 4 — Rule of 3/5: Class có destructor nhưng không có copy constructor/assignment → copy UserCache sẽ double-free mọi User*.

Suggested fix:

cpp
class UserCache {
    std::unordered_map<int, std::unique_ptr<User>> cache_;
public:
    void addUser(int id, const std::string& name) {
        cache_[id] = std::make_unique<User>(id, name);
        // Nếu key đã tồn tại → unique_ptr cũ tự delete
    }

    User* getUser(int id) const {
        auto it = cache_.find(id);
        return it != cache_.end() ? it->second.get() : nullptr;
    }

    void removeUser(int id) {
        cache_.erase(id);  // unique_ptr tự delete User
    }

    // Không cần destructor — Rule of 0!
    // Copy bị disable tự động (unique_ptr không copyable)
};

🔗 Liên kết liên quan

📚 Đọc thêm trong series