Skip to content

Latest commit

 

History

History
313 lines (218 loc) · 10.5 KB

File metadata and controls

313 lines (218 loc) · 10.5 KB
title Memory Safety Hardening
slug memory-safety-hardening
category platform
depth 2
audit_level
2
3
last_reviewed 2026-04-21
sources
Szekeres et al. — SoK: Eternal War in Memory (IEEE S&P 2013)
Clang sanitizer and CFI guidance — https://clang.llvm.org/docs/ControlFlowIntegrity.html
GrapheneOS hardened allocator — https://github.com/GrapheneOS/hardened_malloc
triggers_strong
memory safety
buffer overflow
use after free
asan
ubsan
triggers_weak
native hardening
format string
cfi
relro
fortify
related
desktop-app-security
language-patterns
high-trust-admin-workstations

Memory Safety Hardening

Last reviewed: 2026-04-19 | Next review: 2026-10-19 | Priority: Recommended | Audit Level: 2-3 | Automation: Partial (compiler flags, sanitizer builds, and some warning baselines automatable; exploitability review and unsafe design choices manual)

Use this reference when reviewing C, C++, mixed-language, or native client/server components that parse untrusted input, run with meaningful privilege, or would have high impact if memory corruption became code execution.

A practical lesson from the long-running memory-safety literature is simple: no single mitigation is enough. Low-overhead layered controls are the realistic baseline.


1. What this reference is for

Use it for:

  • native parsers, protocol handlers, and file-processing code,
  • desktop agents or Electron/native helpers with C/C++ dependencies,
  • long-lived daemons handling untrusted network traffic,
  • mixed-language systems where a memory-safe layer wraps unsafe libraries,
  • code review when the question is "is this native component hardened enough to ship?"

Do not use it as a substitute for:

  • application-layer authz or business-logic review,
  • browser-only frontend security,
  • exploit development instructions.

2. Core principle: layered defenses, not one magic flag

A few common myths to avoid:

Defense What it helps with What still breaks it
ASLR alone Randomizes addresses Info leaks and address disclosure
NX / DEP alone Blocks direct code injection ROP and JOP gadget chains
Stack canaries alone Detects many stack overwrites Heap bugs and non-stack corruption
Coarse-grained CFI alone Narrows some control-flow abuse Allowed-call gadget abuse and data-only compromise
Sanitizers in CI alone Catch many bugs before release Gaps in coverage and untested code paths

Baseline conclusion:

  • compile hardened,
  • test with sanitizers,
  • prefer memory-safe languages for new code,
  • reduce unsafe surface area instead of only compensating for it.

3. Compiler and linker baseline

3.1 ELF / Linux baseline

Use a minimum hardening baseline close to:

gcc -O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -pie \
  -Wformat -Wformat-security -Werror=format-security \
  -Wl,-z,relro,-z,now -o app app.c

What each part does:

  • -fstack-protector-strong: protects more functions than the weaker default,
  • -D_FORTIFY_SOURCE=2: adds additional checked libc paths when optimization is enabled,
  • -fPIE -pie: allows full ASLR for the main binary,
  • -Wl,-z,relro,-z,now: reduces GOT overwrite and lazy-binding abuse,
  • -Wformat-security: turns unsafe format-string usage into a build-breaking event.

Also verify host posture:

kernel.randomize_va_space = 2

3.2 Windows baseline

For native Windows builds, expect equivalents such as:

  • /GS for stack cookies,
  • /DYNAMICBASE for ASLR,
  • /NXCOMPAT for DEP,
  • /guard:cf for Control Flow Guard where supported.

3.3 Additional control-flow hardening

Where toolchains allow it, prefer:

  • Clang CFI for supported targets,
  • -fcf-protection=full on compatible Intel platforms,
  • branch-protection features on AArch64 where available.

These controls are especially valuable on code that parses attacker-controlled input or runs with elevated privilege.


4. Sanitizers belong in CI

Sanitizers are usually too expensive for production, but they are highly appropriate in CI, staging, and pre-release test builds.

Recommended baseline:

clang -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 -o app app.c

Use additional sanitizer coverage for targeted components:

  • AddressSanitizer for heap, stack, and use-after-free classes,
  • UndefinedBehaviorSanitizer for overflow and undefined arithmetic/control cases,
  • MemorySanitizer for uninitialized-read-heavy code when feasible.

CI pattern

- name: Build native targets with sanitizers
  run: |
    cmake -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
          -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
          -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" .
    cmake --build .
    ctest --output-on-failure

Review rule:

  • a sanitizer finding is a defect to triage, not a warning to suppress by default,
  • parser and deserializer paths deserve stronger-than-average sanitizer coverage,
  • avoid declaring native code "done" if sanitizer builds are not part of the release path.

5. Dangerous code patterns to flag fast

5.1 Unbounded copies and unsafe formatting

char buf[256];
strcpy(buf, user_input);      // unsafe
gets(buf);                    // always unsafe
printf(user_input);           // format string vulnerability
sprintf(buf, user_input);     // format string vulnerability

Safer patterns:

snprintf(buf, sizeof(buf), "%s", user_input);
printf("%s", user_input);

5.2 Integer overflow feeding allocation or copy length

size_t len = parse_len(user_input);
char *out = malloc(len);
memcpy(out, src, len * 4);    // multiplication may overflow

Review expectation:

  • validate bounds before allocation,
  • validate arithmetic before multiplication or addition,
  • reject impossible or zero-length values explicitly.

5.3 Use-after-free and ownership confusion

Look for:

  • raw pointer lifetime shared across multiple owners,
  • callbacks holding stale references,
  • manual free paths mixed with exceptions or early returns,
  • object pools or caches that blur ownership boundaries.

Prefer:

  • std::unique_ptr and narrow ownership,
  • fewer raw new / delete paths,
  • explicit lifetime comments at FFI boundaries.

6. Heap hardening and allocator choices

For higher-risk native components, heap posture matters.

Useful controls include:

  • detecting heap misuse aggressively in dev and staging,
  • poisoning or perturbing freed memory during tests,
  • considering hardened allocators for high-sensitivity binaries.

Example environment controls for non-production testing:

set MALLOC_CHECK_=3
set MALLOC_PERTURB_=165

Review implication:

  • internet-facing parsers and privileged helpers deserve stronger allocator scrutiny,
  • if the allocator choice is security-relevant, document it and treat it as a control decision,
  • heap hardening does not replace code review or sanitizer coverage.

7. Language strategy for new code

The fastest way to reduce memory-corruption risk is often to avoid creating new unsafe code.

Preferred defaults for new components:

Use case Prefer
New system service Rust
New network daemon Rust or Go
Mobile SDK surface Swift or Kotlin where applicable
Thin wrapper around native library Memory-safe host language with narrow FFI
Parser or file-handling utility Memory-safe implementation where feasible

When C or C++ remains necessary:

  • isolate unsafe modules behind narrow interfaces,
  • keep unsafe blocks small and documented,
  • avoid broad FFI surfaces with unclear ownership,
  • require sanitizer-clean CI for the unsafe portion.

8. Mixed-language and Electron review notes

Many desktop systems are not pure C/C++. They are Electron, .NET, or higher-level applications that still depend on native libraries.

Ask:

  • does a native addon parse attacker-controlled input,
  • does the unsafe component run inside a privileged desktop context,
  • can renderer compromise reach the native layer,
  • are native crash dumps, symbols, and logs handled as sensitive artifacts,
  • is the patching cadence for bundled native dependencies tracked explicitly.

For broader desktop review, pair this file with desktop-app-security.md.


9. Minimum review checklist

Check Expected
PIE / ASLR enabled for native binaries Yes
RELRO and immediate binding enabled where applicable Yes
Stack protector baseline enabled Yes
Format-string warnings enforced at build time Yes
Unsafe copy functions absent or tightly justified Yes
Sanitizer builds run in CI Yes
Parser / deserializer paths receive deeper test coverage Yes
New unsafe modules are justified rather than default choices Yes
Memory-safe language substitution considered for new code Yes
Native dependencies tracked and patched intentionally Yes

10. Review shortcuts

Treat these as high-signal findings:

  • printf(user_input) or equivalent user-controlled format strings,
  • strcpy, strcat, gets, unsafe sprintf,
  • integer arithmetic feeding allocator size without bounds checks,
  • disabled or missing hardening flags on production native binaries,
  • sanitizer findings suppressed without clear triage,
  • broad FFI surfaces with unclear ownership and cleanup rules,
  • privileged desktop helpers built from stale native dependencies.

11. Academic grounding

Szekeres, Payer, Wei, and Song's survey on the "eternal war in memory" is still useful because it explains why stronger defenses often fail to become defaults: performance and deployment cost. The pragmatic takeaway is not to wait for a perfect defense. Ship a stack of low-overhead mitigations that meaningfully raises attacker cost now.

That is why this reference emphasizes:

  • layered compiler and linker hardening,
  • sanitizer-clean CI,
  • selective stronger control-flow protections,
  • reducing new unsafe code through language choice.

12. Companion references

  • desktop-app-security.md for desktop-specific Electron, .NET, and DLL-hijacking issues,
  • language-patterns.md for broader dangerous-code review patterns,
  • high-trust-admin-workstations.md when the native component runs in a privileged operator environment.