Safeguard
Application Security

Modern C++ security: smart pointers, bounds checking, and static analysis

Microsoft has said for years that ~70% of the CVEs it patches trace to memory-safety bugs — here's how modern C++ actually closes that gap.

Safeguard Research Team
Research
6 min read

In December 2023, CISA, the NSA, the FBI, and international partners published "The Case for Memory Safe Roadmaps," urging vendors to move away from memory-unsafe languages and to publish public roadmaps for their legacy C and C++ codebases. A follow-up joint Cybersecurity Information Sheet from CISA and the NSA, "Memory Safe Languages: Reducing Vulnerabilities in Modern Software Development," pushed that guidance further, naming Rust, Go, C#, Java, Swift, Python, and JavaScript as memory-safe alternatives, while the original 2023 guide had already set January 1, 2026 as the target date for vendors to have memory-safety roadmaps in hand. The pressure is not new: Microsoft's own security engineering team has stated repeatedly, going back to a 2019 BlueHat presentation, that roughly 70% of the CVEs it patches each year in its products are memory-safety issues — use-after-free, buffer overflows, and uninitialized reads. Most large C++ codebases cannot be rewritten in a memory-safe language on that timeline, or at all. The realistic path is hardening the C++ that already exists: replacing raw owning pointers with unique_ptr and shared_ptr, replacing raw arrays and pointer-arithmetic with bounds-checkable views like std::span, and running sanitizers and static analyzers as a permanent part of the build. This post walks through what actually works today, not what the standard promises for tomorrow.

Why do smart pointers eliminate whole bug classes instead of just individual bugs?

Smart pointers eliminate whole classes of memory bugs because they tie an object's lifetime to a scope instead of leaving it to a programmer's manual delete call. The C++ Core Guidelines, maintained by Bjarne Stroustrup and Herb Sutter, formally recommend unique_ptr for exclusive ownership and shared_ptr for reference-counted shared ownership, and state plainly that a raw pointer should never be treated as an owner in new code. A unique_ptr cannot be copied, only moved, which makes double-free and use-after-free structurally harder to write rather than just easier to catch in review. shared_ptr's reference count means an object is destroyed exactly when the last owner goes out of scope, closing the dangling-pointer window that opens when one part of a large codebase frees memory another part still holds a raw pointer to. Neither type is free of risk — a shared_ptr cycle still leaks, and weak_ptr exists specifically to break those cycles — but both convert a class of bugs that used to require a debugger to find into a class the compiler and the type system enforce.

What does std::span actually check, and what does it leave unchecked?

std::span, added in C++20, is a non-owning view over a contiguous sequence that carries its length alongside its pointer, which is precisely the information a raw pointer-plus-size argument pair loses at the call site. A function that takes T* data, size_t len has no way to verify at compile time that the two arguments actually describe the same buffer; a function that takes std::span<T> carries both together, and indexing through operator[] in a build with the hardened/checked standard library (or through the bounds-checked .at() accessor being added to span for C++26) turns an out-of-bounds access into a caught error instead of a silent read past the end of the buffer. C++23 followed with std::mdspan for multi-dimensional views, useful for image and tensor data that used to be indexed with manual row-major arithmetic. What span does not do is check that the underlying memory it points to is still alive — it is a view, not an owner, so a span over a vector that gets reallocated or destroyed is exactly as dangerous as a dangling raw pointer. Bounds safety and lifetime safety are separate problems, and span only solves the first one.

What is the "profiles" proposal, and is it something teams can use today?

The "profiles" proposal, led by Bjarne Stroustrup and Gabriel Dos Reis within the ISO C++ committee (WG21), aims to let a codebase opt into named subsets of stricter rules — a bounds-safety profile, a lifetime-safety profile, a type-safety profile — enforced by the compiler on the code marked as compliant, without requiring a full-language rewrite. It is a serious, actively discussed proposal aimed at C++26 and beyond, but as of this writing it has not been fully standardized or shipped in production compilers, and teams should treat it as a direction to watch rather than a tool to depend on this year. The practical takeaway is that the committee's own trajectory — span in C++20, mdspan in C++23, profiles under discussion for C++26 — confirms the standard is moving toward opt-in bounds and lifetime enforcement, which validates adopting bounds-checked types like span and gsl::span now instead of waiting for a future standard to make the decision for you.

Which static and dynamic analysis tools actually catch these bugs before shipping?

A layered toolchain catches what smart pointers and span alone don't. Clang-Tidy and the Clang Static Analyzer walk the AST and symbolic-execution paths to flag use-after-free, null dereferences, and unchecked returns without running the program; Cppcheck does similar lexical and flow-based analysis and is commonly run as a fast pre-commit gate. MSVC's /analyze flag and CodeQL, GitHub's semantic code-analysis engine, both add deeper, queryable data-flow analysis across a whole codebase rather than one file at a time. None of these replace dynamic tools: AddressSanitizer (built into Clang and GCC) instruments a binary to catch out-of-bounds reads and writes and use-after-free at runtime with modest overhead, UndefinedBehaviorSanitizer catches signed-overflow and misaligned-access UB that static tools frequently miss, and Valgrind's Memcheck remains useful where sanitizer instrumentation isn't an option. The combination matters because static tools find bugs on paths that are never executed in test, while sanitizers only find bugs on paths your test suite actually exercises — running both closes gaps the other leaves open.

How should a team actually prioritize this work across a large legacy codebase?

Prioritization should follow the same logic CISA's roadmap guidance implies: start where new code meets old code, not where the codebase is largest. New C++ code should be written against the Core Guidelines from day one — smart pointers for ownership, span for buffer arguments, -Wall -Wextra and Clang-Tidy in CI as a merge gate — because that cost is cheapest paid once, at write time. Legacy code should be triaged by exposure: parsers and network-facing input handling first, since that is where a memory-safety bug becomes a remotely triggerable vulnerability rather than a crash on malformed local input. Running AddressSanitizer and UndefinedBehaviorSanitizer builds through an existing fuzzing or integration-test suite finds live bugs in that exposed code far faster than a line-by-line audit. Safeguard's own SAST engine currently covers JavaScript/TypeScript, Python, and Java in its first phase, with C and C++ support planned but not yet available — so for now, a C++ team's static-analysis layer is Clang-Tidy, Cppcheck, and CodeQL, while Safeguard's software composition analysis and SBOM generation still give visibility into the third-party C++ libraries and system dependencies that a memory-safety push has to account for alongside the first-party code.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.