The Clean Code Betrayal
It was never about Mozart. I just like little touches. Sometimes from a book, sometimes from a recital.

It was never about Mozart. I just like little touches. Sometimes from a book, sometimes from a recital.

Executive Summary

This research shows how modern compilers (GCC/Clang) can remove security mechanisms during aggressive optimization phases "not only by assembly analysis, but practically at runtime".

1. Introduction

1.1 Problem Definition

In security-critical applications, sensitive data (passwords, cryptographic keys, API tokens) must be erased from memory after use. Developers often use the memset() function in the C standard library for this purpose:

void process_password(const char *password) {
    char local_copy[64];
    strcpy(local_copy, password);
    
    // Process
    authenticate(local_copy);
    
    // Security: Remove Memory
    memset(local_copy, 0, sizeof(local_copy));
}

However, modern compilers apply the following logic when optimizing this code:

"local_copy variable is never used after memset() → This write operation is unnecessary → Remove!"

This optimization is known as "Dead Store Elimination (DSE)" and is fully valid according to the C/C++ standard. However, it creates a security-critical vulnerability.

1.2 Scope of Research

In this research:

  1. Demonstrated how the DSE vulnerability can be exploited practically, not theoretically.
  2. The different behaviors of memset() and explicit_bzero() functions at same optimization level (-O3) are compared.
  3. Stack Frame Reuse technique was used to realize sensitive data leakage at runtime.
  4. Real-world CVEs are analyzed and solutions are proposed.

2. Background

2.1 What is Dead Store Elimination?

Dead Store Elimination (DSE) is one of the compiler optimization techniques. If the value written to a variable is never read afterwards, this write operation is considered "dead store" and is removed.

Dead Store Elimination
Dead Store Elimination Visualization

Example

int calculate() {
    int result = expensive_computation();  // Expensive computation
    result = 42;                           // Previous value not used
    return result;
}

The compiler removes the call to expensive_computation() because its result is not used.

2.2 DSE in Security Context

DSE in safety-critical codes is problematic:

void handle_secret(const char *secret) {
    char buffer[64];
    strcpy(buffer, secret);
    
    use_secret(buffer);
    
    memset(buffer, 0, 64);  // ← This line remove
}

Compiler's point of view:

  • buffer variable not read after memset()
  • memset() only changes memory (no side effect)
  • Result: memset() call is removed

2.3 Standard C and Undefined Behavior

The C standard (ISO/IEC 9899) grants the compiler permission to optimize as long as "observable behavior" doesn't change. Removing the memset() call is a valid optimization according to the standard because:

  1. Function return value doesn't change
  2. Global state doesn't change
  3. No I/O operation

Therefore, GCC Bug #8537 (2002) was marked as "WONTFIX".

3. Methodology

3.1 Test Environment

Platform: Linux x86_64 (Ubuntu 24.04 LTS)

Compiler: GCC 13.3.0

Optimization: -O3 (for both versions)

Additional Flags: -fno-stack-protector (stack canary disabled)

3.2 Stack Frame Reuse Technique

Traditional methods (assembly analysis, binary string search) demonstrate the vulnerability theoretically. This research proves that the vulnerability can be practically exploited at runtime.

Technique:

  1. Victim function writes sensitive data to stack
  2. Memory clearing function is called (memset() or explicit_bzero())
  3. Function returns (stack frame "pops")
  4. Hacker function uses the same stack area
  5. Uninitialized variable reads old data from stack

Critical Point: If the clearing function was removed by the compiler, old data still exists on the stack and can be read.

3.3 Code Design

3.3.1 Vulnerable Version (memset)

#include <stdio.h>
#include <string.h>

__attribute__((noinline, optimize("O3")))
void victim_function() {
    char padding[8];
    char secret[32];
    volatile char *ptr = secret;
    
    memset(padding, 0, 8);
    strcpy((char*)ptr, "SECRET_PASSWORD_LEAKED");
    printf("[VICTIM] Secret written: %s\n", secret);
    
    memset(secret, 0, 32);  // ← Compiler will remove this!
}

__attribute__((noinline, optimize("O3")))
void hacker_function() {
    char padding[8];
    char stolen[32];  // ← Not initialized!
    
    memset(padding, 0, 8);
    
    printf("[HACKER] Reading stack: ");
    for(int i=0; i<32; i++) {
        char c = stolen[i];
        if(c >= 32 && c < 127) putchar(c);
        else if(c == 0) break;
    }
    printf("\n");
}

int main() {
    victim_function();
    hacker_function();
    return 0;
}

3.3.2 Secure Version (explicit_bzero)

// ... (same code, only one line difference)

__attribute__((noinline, optimize("O3")))
void victim_function() {
    // ...
    explicit_bzero(secret, 32);  // ← Compiler can NEVER remove this!
}

3.3.3 Design Decisions

1. __attribute__((noinline, optimize("O3")))

  • Function is not inlined → Stack frame preserved
  • O3 optimization applied at function level

2. volatile char *ptr

  • Prevents strcpy() operation from being removed
  • But doesn't protect memset() (this is intentional!)

3. char padding[8]

  • Ensures stack alignment
  • Victim and hacker functions work at same offset

4. Uninitialized variable

  • char stolen[32]; → No value assigned
  • Reads garbage data from stack

5. -fno-stack-protector

  • Stack canary disabled
  • Ensures clean stack frame overlap

4. Experimental Results

4.1 Compilation

gcc -O3 -fno-stack-protector -o vulnerable vulnerable.c
gcc -O3 -fno-stack-protector -o secure secure.c

4.2 Runtime Output

Runtime Output
Runtime Output Comparison

4.3 Assembly Analysis

Vulnerable (memset):

$ objdump -d vulnerable | grep "memset@plt"
# Output: EMPTY

memset() calls: 0 (removed!)

Secure (explicit_bzero):

$ objdump -d secure | grep "bzero"
# Output: __explicit_bzero_chk

explicit_bzero() calls: 1 (preserved!)

Assembly Analysis
Assembly Analysis Comparison

4.4 Comparison Table

Metric vulnerable.c secure.c
Optimization Level -O3 -O3
Clearing Function memset() explicit_bzero()
Assembly Call Count 0 1
Runtime Leak ✅ YES ❌ NO

5. Findings

5.1 Compiler Behavior

Vulnerable: memset() → Dead Store Elimination → Removed (0 calls)

Secure: explicit_bzero() → POSIX guarantee → Preserved (1 call)

5.2 Real-World CVEs

CVE Project Impact CVSS Fix
CVE-2016-0777 OpenSSH SSH key leak 7.5 explicit_bzero()
CVE-2023-5217 libvpx Memory disclosure 8.8 bzero()
CVE-2024-38616 Linux Kernel WiFi password leak 5.5 memzero_explicit()
GCC Bug #8537 GCC WONTFIX (2002) - Developer responsibility

6. Conclusion

Key Findings:

  1. memset() and explicit_bzero() behave differently at the same O3 level
  2. memset() is removed → Runtime leak
  3. explicit_bzero() is preserved → Secure
  4. Active CVEs exist even in 2024

Recommendations:

  • Don't use memset() in security-critical code
  • Use explicit_bzero() / memset_s() / SecureZeroMemory()
  • Verify assembly output

References

  1. POSIX.1-2024 - explicit_bzero() Specification
  2. CWE-14: Compiler Removal of Code to Clear Buffers
  3. CERT C - MSC06-C: Beware of compiler optimizations
  4. CVE-2016-0777, CVE-2023-5217, CVE-2024-38616
  5. GCC Bug #8537 (2002, WONTFIX)