Critical Buffer Overflow Fixed: When "Safe" Functions Aren't Safe
Introduction
Imagine you install a deadbolt on your front door, feel secure, and go to sleep — only to discover that someone secretly replaced the lock mechanism with a hollow prop that looks identical from the outside. That's essentially what was happening in DeepSkyStackerKernel/StackWalker.cpp.
A critical vulnerability (V-001, CWE-120) was discovered where preprocessor macros were silently redefining the safe CRT string functions strcpy_s and strcat_s to their unsafe counterparts strcpy and strcat. Developers writing code believed they were using memory-safe functions. They were not.
This kind of vulnerability is particularly dangerous because it's invisible at the call site. Every engineer reading the code sees strcpy_s and reasonably assumes bounds checking is in place. The deception happens at the preprocessor level, long before a human eye or code reviewer would typically look.
If you write C or C++ code — especially in security-sensitive applications — this post is essential reading.
The Vulnerability Explained
What Are strcpy_s and strcat_s?
Microsoft introduced the "safe" CRT functions (strcpy_s, strcat_s, sprintf_s, etc.) as part of the Secure CRT library to address the classic C string handling problem: unbounded memory writes.
The classic strcpy function copies a string from a source to a destination buffer with absolutely no regard for how large the destination buffer is:
// UNSAFE: No bounds checking whatsoever
char dest[16];
strcpy(dest, some_attacker_controlled_string); // 💥 Potential overflow
The safe variant strcpy_s requires the caller to specify the destination buffer size:
// SAFE: Bounds are checked; fails gracefully if exceeded
char dest[16];
strcpy_s(dest, sizeof(dest), some_attacker_controlled_string); // ✅ Protected
If the source string is too long, strcpy_s triggers a constraint handler (typically aborting the program) rather than silently overwriting memory. This is a critical safety guarantee.
The Malicious (or Negligent) Macro
The vulnerability in StackWalker.cpp involved preprocessor macro definitions that looked something like this:
// THE VULNERABLE CODE (conceptual representation)
// Defined somewhere in StackWalker.cpp around line 229
#define strcpy_s(dest, size, src) strcpy(dest, src)
#define strcat_s(dest, size, src) strcat(dest, src)
Notice what these macros do:
- They accept the three-argument signature of the safe functions (including the size parameter)
- They silently discard the size argument entirely
- They delegate to the unsafe two-argument versions
This means every single call to strcpy_s or strcat_s in any file that includes or is compiled after this macro definition is silently converted to an unsafe, unbounded string operation. The buffer size argument is thrown away like it never existed.
How Does This Happen in Practice?
The C preprocessor runs before compilation and performs textual substitution. When the compiler sees:
strcpy_s(filename_buffer, MAX_PATH, user_supplied_path);
After macro expansion, it actually compiles:
strcpy(filename_buffer, user_supplied_path);
The MAX_PATH argument vanishes. The compiler never sees it. No warning is generated. The resulting binary contains an unsafe string copy.
What Is a Buffer Overflow, and Why Does It Matter?
A buffer overflow occurs when a program writes data beyond the allocated boundary of a buffer. In C/C++, this results in undefined behavior — but in practice, it means overwriting adjacent memory.
Depending on what's adjacent in memory, an attacker who can control the overflowing string can:
- Overwrite return addresses on the stack → redirect execution to attacker-controlled code
- Overwrite function pointers → hijack control flow
- Overwrite adjacent variables → manipulate program logic
- Cause crashes → denial of service
This class of vulnerability (CWE-120: Buffer Copy Without Checking Size of Input) has been responsible for some of the most devastating exploits in computing history, from the Morris Worm (1988) to modern RCE vulnerabilities.
Real-World Attack Scenario
DeepSkyStacker processes astronomical image files and metadata. Consider this attack path:
- An attacker crafts a malicious image file with an excessively long filename, EXIF metadata field, or embedded symbol name
- The application reads this data into a string variable
- Code that appears to safely call
strcpy_s(buffer, sizeof(buffer), metadata_field)is actually callingstrcpy(buffer, metadata_field)due to the macro - The oversized string overflows the buffer, overwriting the stack frame
- With careful crafting, the attacker overwrites the return address to point to shellcode or a ROP chain
- When the function returns, execution jumps to attacker-controlled code
Even without full code execution, a reliable crash can be weaponized as a denial-of-service attack against the application.
The Fix
What Changed
The fix removes the dangerous macro definitions from StackWalker.cpp. By eliminating these #define statements, the actual strcpy_s and strcat_s implementations from the secure CRT library are restored throughout the codebase.
Before (Vulnerable):
// These macros silently neutered all safe string function calls
#define strcpy_s(dest, size, src) strcpy(dest, src)
#define strcat_s(dest, size, src) strcat(dest, src)
After (Fixed):
// Macros removed — strcpy_s and strcat_s now call the real secure CRT implementations
// No redefinition. The safe functions work as intended.
Why This Fix Works
By removing the macro definitions:
strcpy_scalls now reach the real implementation in the secure CRT library, which performs bounds checking- The
sizeargument is no longer discarded — it's used to validate the copy operation - Constraint handling is restored — if a string would overflow, the runtime handles it safely rather than silently corrupting memory
- Every call site in the codebase is protected — since the macros affected all downstream code, removing them restores safety everywhere simultaneously
This is a high-leverage fix: one change in one file restores memory safety across potentially hundreds of call sites.
The Broader Security Principle
This vulnerability illustrates an important lesson: security guarantees can be undermined at layers below where developers are looking. A code reviewer examining a function that calls strcpy_s would reasonably conclude it's safe. The vulnerability existed at the preprocessor layer, which most developers never inspect during routine review.
Prevention & Best Practices
1. Audit Preprocessor Macro Definitions
Never redefine standard library security functions with unsafe equivalents. Establish a policy that macros overriding CRT functions are forbidden and enforce it in code review.
Use grep or static analysis to detect such patterns:
# Search for dangerous macro redefinitions
grep -rn "#define strcpy" .
grep -rn "#define strcat" .
grep -rn "#define sprintf" .
grep -rn "#define gets" .
2. Enable Compiler Warnings and Treat Them as Errors
Modern compilers can catch some of these issues. Use aggressive warning levels:
# CMake example
target_compile_options(your_target PRIVATE
-Wall
-Wextra
-Werror
/W4 # MSVC
/WX # MSVC: warnings as errors
)
3. Use Static Analysis Tools
Tools like these can detect unsafe string operations and suspicious macro definitions:
- Clang-Tidy with
bugprone-*andcert-*checks - Coverity — specifically detects CWE-120 patterns
- PVS-Studio — strong C/C++ static analyzer
- CodeQL — GitHub's semantic analysis platform
- Semgrep — customizable pattern matching for security rules
Example Semgrep rule to detect this pattern:
rules:
- id: unsafe-strcpy-macro-redef
pattern: |
#define strcpy_s(...) strcpy(...)
message: "Dangerous redefinition of safe CRT function"
severity: ERROR
languages: [c, cpp]
4. Prefer Modern C++ String Handling
Where possible, use std::string instead of raw character arrays:
// Prefer this:
std::string filename = user_input; // No buffer overflow possible
// Over this:
char filename[MAX_PATH];
strcpy_s(filename, sizeof(filename), user_input.c_str()); // Still risky
std::string dynamically manages its own memory and cannot overflow in the traditional sense (it may throw std::bad_alloc under extreme conditions, but won't corrupt adjacent memory).
5. Use AddressSanitizer During Development and Testing
AddressSanitizer (ASan) is a compiler instrumentation tool that detects buffer overflows at runtime:
# Compile with AddressSanitizer
clang++ -fsanitize=address -g -o your_app your_app.cpp
# Or with GCC
g++ -fsanitize=address -g -o your_app your_app.cpp
Running your test suite with ASan enabled will catch buffer overflows that static analysis might miss.
6. Understand the Relevant Standards
- CWE-120: Buffer Copy without Checking Size of Input ("Classic Buffer Overflow") — cwe.mitre.org/data/definitions/120.html
- OWASP: Buffer Overflow — owasp.org/www-community/vulnerabilities/Buffer_Overflow
- SEI CERT C Coding Standard: STR07-C — Use the bounds-checking interfaces for string manipulation
- Microsoft SDL: Banned function list explicitly prohibits
strcpy,strcat, and related unsafe functions
7. Establish a Banned Functions Policy
Microsoft publishes a list of banned C/C++ functions that should never appear in production code. Enforce this with a pre-commit hook or CI check:
# Functions banned by Microsoft SDL
strcpy, strcat, sprintf, gets, scanf (with %s), ...
Any occurrence of these functions should require explicit justification and security review.
Conclusion
This vulnerability is a masterclass in how subtle and dangerous preprocessor abuse can be. A two-line macro definition silently stripped memory safety protections from an entire codebase, turning every "safe" string operation into a potential buffer overflow — all while looking perfectly fine to anyone reading the code.
The key takeaways for developers:
- Preprocessor macros can undermine security guarantees invisibly — audit them carefully, especially in legacy codebases
- Never redefine standard security functions — this is a red flag in code review, whether intentional or accidental
- Defense in depth matters — use static analysis, runtime sanitizers, and code review together, because no single technique catches everything
- The safe CRT functions exist for a reason —
strcpy_s,strcat_s, and their siblings are not optional inconveniences; they are essential protections - High-leverage fixes are worth celebrating — removing these two macro definitions simultaneously restored safety across hundreds of call sites
Buffer overflows have been a known vulnerability class for over four decades, yet they remain consistently in the OWASP Top 10 and CWE Top 25. The reason isn't that developers don't know about them — it's that the C/C++ language makes it remarkably easy to introduce them, sometimes through mechanisms as subtle as a #define in a utility file.
Write safe code. Review your macros. Trust but verify your safety functions.
This vulnerability was identified and fixed by OrbisAI Security. Automated security scanning combined with LLM-assisted code review confirmed both the vulnerability and the fix.