06 November 2026

How to Prevent Buffer Overflow: Secure Coding Steps

Reza Khosravi

Table of Contents

How to Prevent Buffer Overflow: Secure Coding Steps

You're reviewing a parser change that looks harmless: a larger input field, a new copy operation, and a passing test suite. The destination buffer hasn't changed, though, and nobody added a test for an oversized value. That's the gap where a routine feature change can introduce a buffer overflow.

Learning how to prevent buffer overflow means doing more than replacing dangerous functions. You need a defensible length check, an API that preserves it, and a required test that fails when the next change breaks the assumption.

Unsafe Functions and Their Safer Replacements

Start with copy and formatting calls that trust input length. A bounded replacement gives the operation a destination limit, but you still need to check whether the result fits and whether truncation changes its meaning.

Unsafe calls and their bounded replacements

Unsafe callBounded replacementWhy it failsCheck to add
strcpy(dst, src)snprintf(dst, sizeof dst, "%s", src)Copies until NUL without a destination limitCheck for a negative result, then compare the result with sizeof dst
strcat(dst, src)snprintf or strncat with remaining spaceAssumes dst has room for srcCalculate remaining capacity before calling
gets(buf)fgets(buf, sizeof buf, stdin)Has no input-length limitCheck success, strip a trailing newline, and handle a missing newline
sprintf(buf, fmt, ...)snprintf(buf, sizeof buf, fmt, ...)Formatted output can exceed the bufferReject a negative result or a result greater than or equal to capacity
scanf("%s", buf)A width-limited conversion, such as scanf("%31s", buf) for a hypothetical char buf[32]Reads an unbounded tokenKeep the width below capacity to reserve the terminator
memcpy(dst, src, n) with input-derived nValidate n before copyingTrusts an attacker-controlled lengthReject lengths exceeding destination capacity or readable source bytes
array[i] in C++ parsing codearray.at(i) or an explicit bounds checkOrdinary unchecked subscripting doesn't validate the indexCheck input-derived indices and handle rejection

For strcpy, the snprintf replacement works only when src is already a valid, terminated string. A packet containing arbitrary bytes needs an explicit source length, not %s.

For appends, first establish that the destination is terminated within its capacity. Then calculate available space from its current length; strncat takes an append limit, not the destination's total size.

With fgets, a missing newline can mean the input continues beyond the buffer or ends at EOF. Decide whether to reject or consume the remainder so the next read doesn't interpret a leftover fragment as a new field.

Bounded doesn't automatically mean correct

snprintf reports the output length it would have produced, excluding the terminator. Check for a negative return before converting it to size_t; a nonnegative result at least as large as capacity means truncation.

A width-limited scanf conversion also leaves excess input unread. Check its assignment count and define how the parser handles the remainder.

Don't mechanically substitute strncpy: it doesn't guarantee termination when the source fills the specified count. strlcpy can simplify string copying where available, including BSD-derived systems and some Linux libraries, but it isn't ISO C.

Review rule: Every fixed-buffer write needs an explicit capacity argument or a nearby, demonstrably valid bounds check.

Make that rule part of your secure coding best practices, rather than relying on reviewers to remember each function's quirks.

How to Check Length Before Every Copy

A correct copy proves that the source is readable, the destination is writable, and the operation preserves the field's meaning. A length supplied by a packet isn't proof of any of those conditions.

A four-step infographic illustrating how to safely check buffer lengths to prevent data overflows during copying.

  1. Identify the destination capacity. Use sizeof for an actual array in the current scope, and retain an allocation-size field for heap storage. For strings, capacity includes space for the terminating NUL.

  2. Establish the readable source length. For packet data, validate the declared length against bytes available before inspecting or copying the field. For text input, use a bounded read or a bounded terminator search rather than an unrestricted strlen on untrusted memory.

  3. Compare against remaining capacity. For an append, validate the existing destination length before subtracting it from capacity. Prefer comparisons such as requested > capacity - used - 1 after proving used < capacity, instead of adding lengths that could overflow.

  4. Perform the bounded operation. Call memcpy only after validating both source availability and destination space, then add the terminator when the destination represents a string. Remember that overlapping regions require memmove, not memcpy.

  5. Check the operation's result. For formatted output, reject formatting errors and truncation. Parsing code shouldn't accept a shortened username, path, identifier, or authorization value.

  6. Commit only validated data. Build the result in temporary storage, validate it completely, and only then update shared application state. Return a clear error without leaving a partially populated structure that downstream code might trust.

A packet-length example

Suppose a protocol carries a username length and payload, and your destination is a hypothetical 64-byte username array. Its string payload limit is therefore 63 bytes, with the remaining byte reserved for NUL.

Validate that the declared username length matches the actual field payload, then reject anything above the destination's payload limit. If a display-only preview explicitly permits truncation, clamp its copy length to that limit after validating the packet, but don't truncate the authoritative username.

Add tests for inconsistent declared lengths, oversized values, embedded NUL bytes, and empty input according to the protocol. A packet can fit in memory and still be invalid for the application.

A reviewer should see the capacity, the validated source length, the rejection branch, and the write together.

Watch for sizeof on a pointer parameter. It returns the pointer's size, not the caller's buffer capacity, so a helper accepting char *dst must receive capacity separately or use a length-carrying type.

Safer Types and Languages That Remove Manual Bounds Math

When every caller must reconstruct pointer-and-length relationships, change the interface rather than adding another comment. Types that carry their own size reduce opportunities for those values to diverge.

A programmer considering safer modern C++ data structures like std::string, std::vector, and std::array to prevent errors.

Choose abstractions that preserve the boundary

In C++, use std::string for owned text and std::vector for dynamically sized byte sequences. Use checked at() access where appropriate, or range-based loops when you don't need indices; changing the container without changing unchecked access doesn't finish the job.

std::array preserves fixed-size information better than a raw array that decays into a pointer. std::span carries a pointer and extent across interfaces, but doesn't own the storage or guarantee its lifetime, and you shouldn't assume its subscripting performs runtime bounds checks.

Rust's safe slice indexing panics on out-of-range access rather than permitting an unchecked write. Iterators avoid much index arithmetic, although panic behavior still needs an availability decision for services handling hostile input.

Rust's unsafe blocks permit operations whose safety obligations the programmer must establish manually; they don't switch off all language checks. For teams assessing ownership, interoperability, and adoption, Cleffex Digital ltd.’s Rust insights for development teams provides additional context.

Java and C# arrays throw on out-of-bounds access in ordinary managed code. Review native interop, unsafe facilities, and external libraries separately, because managed callers don't establish the safety of native implementations.

Keep unavoidable raw memory behind an interface

For C, introduce a small buffer struct containing a data pointer, length, and capacity. Route writes through a bounded-copy helper that validates those fields and has a documented overlap and termination policy.

A new parser or service boundary may be a realistic memory-safe-language candidate. An existing C library with a required ABI may instead need a narrow wrapper and an explicit replacement plan.

  • Prefer iteration. Use iterators or range-based loops instead of manually advancing indices.
  • Contain pointer arithmetic. Keep it inside a narrowly scoped module with testable contracts.
  • Review unsafe operations. Require justification and invariant checks for every unsafe block or native boundary.

Compiler and Runtime Defenses to Turn On

Turn these on in the build you ship, then prove they stayed on. A flag that only exists in a wiki page will drift off in the next refactor.

Compiler and runtime defenses and what each stops

DefenseWhat it stopsLimit
-fstack-protector-strongDetects canary corruption associated with some stack overwrites before returnCan miss adjacent-field corruption and unprotected functions
-D_FORTIFY_SOURCE=3 with -O2Detects some overflowing checked libc operationsRequires supported tooling and usable object-size information
-fPIE with -pie and ASLRReduces predictable executable addressesDoesn't stop the overflow write
NX or DEPPrevents execution from pages marked nonexecutableDoesn't prevent data corruption
-fcf-protectionSupports restrictions on certain forged control-flow transfersRequires compatible CPU, operating-system, and toolchain support
/GS and Control Flow GuardProvides Windows stack-cookie and indirect-call protectionsRequires toolchain-specific configuration and coverage

These defenses mitigate exploitation. They do not fix the write. A stack canary may fire when an overwrite crosses it before return, while a neighboring struct field gets corrupted unnoticed. ASLR moves addresses around. The invalid write still happened.

That gap is why this work belongs in requirements, not patch day. Microsoft reported that approximately 70% of the vulnerabilities it assigned and fixed from 2006 through 2018 were memory-safety issues. I recommend treating each flag above as a control with an owner and a check, not a one time hardening ticket.

Configure the target, then inspect the artifact

Apply flags to production targets, including native libraries built from separate repos or vendored source. A hardened executable does not harden its dependencies.

For recent Linux baselines, the Fedora 45 release news on NeoTeo gives one example of how distribution defaults move over time. Verify your own shipped binary locally regardless of what any distribution enables by default.

  • Stack protection. Enable -fstack-protector-strong on supported GCC or Clang builds.
  • Library checks. Use -D_FORTIFY_SOURCE=3 with optimization where supported. Confirm it is active in the build log instead of assuming a fallback kept it.
  • Executable layout. Set compilation and linking options together so the binary can participate in ASLR.
  • Windows builds. Confirm /GS and Control Flow Guard in the effective compiler and linker settings, not just in project documentation.

Fortification only helps when the compiler can see object size. Opaque pointers across module boundaries often leave it blind, so expect thin coverage there and keep explicit length checks at those boundaries.

Close the loop with a check you can rerun. Build a small test program with a runtime-sized unchecked copy and record which configurations abort. Confirm the compiler did not optimize the test away. NX and ASLR will not abort on overflow, so assert only what each defense promises. Store the build commands plus checksec or equivalent output where CI can fail the merge when a flag drops.

Testing Boundaries With Sanitizers and Fuzzing

Sanitizers flag bad memory accesses on paths your tests execute; fuzzers search for inputs that reach those paths. Use them together, and judge a fix by the regression test that exercises its original failure condition.

The scope of native-code exposure makes selective testing important: in its joint analysis of 172 critical open-source projects, CISA and partner agencies found that 52% contained code written in memory-unsafe languages. Prioritize the components that accept hostile bytes, rather than instrumenting only convenient unit-test targets.

A diagram illustrating a four-step process for using sanitizers and fuzzing to identify buffer overflow vulnerabilities.

Instrument the code that performs the write

Build a test configuration with AddressSanitizer, typically using -fsanitize=address and -fno-omit-frame-pointer. It detects many heap, stack, and global out-of-bounds accesses, but it isn't proof that every access within a larger allocated object is semantically valid.

Use MemorySanitizer for uninitialized reads in a separate, appropriately instrumented build. Instrument dependencies where necessary; otherwise, missing instrumentation can undermine useful results.

Add UndefinedBehaviorSanitizer with -fsanitize=undefined for related faults such as signed overflow and misaligned accesses. It doesn't check every arithmetic mistake, so explicit length validation remains necessary.

Give each parser a reachable fuzz target

A libFuzzer or AFL++ harness should accept a byte buffer and its length, then invoke the parser. Avoid a harness that rejects nearly everything before reaching parsing logic, and reset state so repeated inputs remain independent.

  • Seed meaningful paths. Include valid messages, minimal fields, malformed lengths, and inputs near capacity boundaries.
  • Test exact edges. Exercise capacity minus one, capacity, capacity plus one, empty input, and maximum representable length fields.
  • Model extreme lengths safely. Encode a huge declared length in a small packet rather than allocating an enormous source buffer.
  • Preserve failures. Minimize each crashing input, repair the defect, and retain the input as a deterministic regression test.

Assert behavior, not merely survival. An oversized identifier should produce the intended rejection and leave the destination state unchanged.

A clean sanitizer run establishes what happened on executed paths, not what could happen on every possible input.

Complement execution tests with the Clang Static Analyzer, CodeQL, or Coverity, focusing review on newly introduced unsafe calls and changed length relationships. Have a developer manually examine parser arithmetic, signed conversions, and error branches: fuzzers, sanitizers, and static analyzers miss different defects.

Fixing Legacy C and C++ Code in Stages

Suppose a C++ desktop application opens PDF attachments received through email. Its parser handles untrusted content inside the application process, while much of the remaining code manages ordinary UI state. Isolating and fuzzing that parser is a more defensible first investment than beginning a broad UI rewrite.

Prevention and mitigation aren't interchangeable. OWASP's buffer-overflow guidance describes abstraction libraries, compiler canaries, and operating-system defenses as “not a complete solution,” while recommending automatic bounds checking at the language or compiler level.

Prioritize exposure rather than file count

  1. Inventory native components and dependencies. Include vendored libraries, generated bindings, plugins, and separately compiled modules. Record who owns each component and how updates reach production.

  2. Classify the entry points: network traffic, untrusted files, and privileged operations. For the PDF example, document how an attachment reaches the parser and what resources the parser process can access.

  3. Harden the supported build immediately. Apply the baseline across relevant targets and document unsupported settings. Give each exception an owner and a reason, not an indefinite waiver.

  4. Put sanitizer tests and fuzz harnesses around the highest-risk paths. Start with attachment decoding, object-length handling, and decompression boundaries rather than unrelated internal helpers.

  5. Replace or isolate exposed components. A restricted worker process can limit consequences while a replacement is developed. A memory-safe parser may remove particular unsafe operations, but any native libraries it calls remain separate review targets.

  6. Track the share of exposed processing moved into safer implementations or restricted processes. Record which entry points no longer reach unsafe code directly, rather than counting edited files.

Isolation also has costs: process supervision, serialization, error handling, and restrictions on filesystem access all require engineering. Define the worker's privileges and validate messages on both sides of the boundary; moving the same unchecked parser into an unrestricted helper isn't meaningful containment.

Use a repeatable source-code audit process to connect the inventory to actual call paths and dependency versions. Re-check the exposure list at each release, especially when a new file format, plugin, or integration changes what the application accepts.

Enforce Bounds Checks in Your CI Pipeline

Buffer-safety checks should block unsafe merges, not merely produce reports. CISA's secure-by-design alert on eliminating buffer-overflow vulnerabilities recommends layered testing and compiler protections while warning that instrumented testing detects many, but not all, memory-safety issues.

Make the requirements executable:

  1. Run relevant instrumented jobs. Require sanitizer tests and bounded fuzz runs when a pull request changes a parser, copy routine, or its dependencies. Keep longer fuzz campaigns outside the merge-critical path.

  2. Fail a hardened target when required flags disappear. Inspect effective commands and use checksec or a platform equivalent for artifact properties; binary inspection alone can't prove every source-level setting.

  3. Flag new unsafe calls. Add static rules for prohibited copying and formatting APIs. Require a written justification, compensating control, and reviewer approval for exceptions.

  4. Require boundary tests or a fuzz harness for each new input field. The tests should establish both rejection behavior and preservation of valid application state.

  5. Enforce and retain results. Block merges when required policies fail, and preserve the commit, configuration, job outcome, and approved exception record.

Keep the evidence connected

Link the threat model, design decision, code change, test evidence, policy result, and deployment. Each record should identify its predecessor so a reviewer can establish which control ran against which revision.

Include interface changes, imported libraries, generated code, and AI-written patches in the same CI/CD pipeline security rules. Those changes can invalidate a previously sound capacity assumption without introducing an obvious unsafe function.

We enforce Policy-as-Code on every pull request, with the option to block unsafe merges, and tie identified design gaps and recommended controls to concrete checks your pipeline can run. Outputs must not be relied on as legal, security, or compliance advice; your team remains responsible for evaluation and testing.

This sprint, select your most exposed parsers, add their boundary tests and fuzz harnesses, and enable merge blocking once those jobs pass reliably.


DevArmor connects security design requirements to implementation checks so buffer-handling decisions remain verifiable as code changes. Visit DevArmor to explore continuous threat modeling, security reviews, and pull-request policy enforcement for your development workflow.

Table of Contents

Subscribe