C Memory Safety skill for Codex
Reproduce under ASan, fix the cause, kill the pattern. Codex treats a C memory bug as fixed only when a sanitizer reproduces it, a regression test captures it, and the same pattern is removed elsewhere. It picks the right tool per bug class, reads ASan, UBSan, MSan and Valgrind reports with a bundled guide, replaces unsafe string and size arithmetic with checked patterns, and adds a libFuzzer target for code that parses untrusted input.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- c-memory-safety Installs to ~/.agents/skills/c-memory-safety/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install c-memory-safety The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project c-memory-safety Installs to .agents/skills/c-memory-safety/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/c-memory-safety.zipunzip -o c-memory-safety.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) c-memory-safety PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $c-memory-safety, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Finds and fixes memory bugs in C - buffer overflows, use-after-free, double free, leaks, uninitialised reads, integer overflow in size math, format string and string-handling bugs - using AddressSanitizer, UndefinedBehaviorSanitizer, MemorySanitizer, Valgrind, static analysers and libFuzzer, then hardens with safe patterns and compiler flags. Use when a C program crashes or corrupts data intermittently, a sanitizer or Valgrind report needs interpreting, or C code that parses untrusted input needs review.
Use it when
- A C program segfaults, corrupts data or behaves differently between builds
- An ASan, UBSan, MSan or Valgrind report needs interpreting
- C code parses files, network packets or other untrusted input
- Adding sanitizer and hardening flags to a C build
Use something else when
- The code is C++ and the question is ownership design: use cpp-modern
- The target is a microcontroller without sanitizer support: use embedded-c
- You are reviewing unsafe blocks in Rust FFI: use rust-unsafe-review
How to check the result
A regression test fails before and passes after the fix under ASan and UBSan with halt_on_error=1, and the full suite runs clean with those sanitizers.
The mistake it prevents
Adding a NULL check or a bigger buffer next to the crash site, which hides the symptom while the size computation or ownership bug remains.
A memory bug is fixed when a test reproduces it under a sanitizer, the fix makes that test pass, the whole suite is clean under ASan and UBSan, and the pattern that caused it is removed everywhere it appears, not only at the crashing line. “It stopped crashing” is not evidence.
Before changing code
- Read
AGENTS.md, the build files (Makefile, CMake, Meson) and CI config. Note the compilers, the C standard (-std=c11,c17,c23) and existing warning flags. - Build as-is and record warnings; many memory bugs are already flagged by
-Wall -Wextra. - Get a reproduction: failing input, test or command. If the bug only shows in production, ask for a core dump, the input that triggered it, or the sanitizer log.
Workflow
- Build a sanitizer configuration (below) in a separate build directory.
- Reproduce under ASan+UBSan; save the report.
- Read the report: access type, size, the faulting stack, and the allocation and free stacks. Details in references/sanitizer-reports.md; read it when a report is unfamiliar or confusing.
- Write a regression test that triggers the bug (unit test or a saved fuzz input).
- Fix the root cause: ownership, bounds or size computation. Not a NULL check sprinkled near the crash.
- Grep for the same pattern elsewhere (
rg -n 'strcpy|sprintf|alloca|\bgets\b', repeatedmalloc(n * size)). - Run the full suite under ASan+UBSan, then MSan or Valgrind if uninitialised reads are plausible.
- If the code parses untrusted input, add or extend a fuzz target.
Choosing the tool
| Bug class | Tool | Build flags |
|---|---|---|
| Overflow, use-after-free, double free, stack bugs | AddressSanitizer | -fsanitize=address -fno-omit-frame-pointer -g -O1 |
| Signed overflow, shifts, misaligned access, bad casts, null deref | UBSan | -fsanitize=undefined -fno-sanitize-recover=all |
| Reads of uninitialised memory | MemorySanitizer (Clang, Linux only; all code incl. libs must be instrumented) or Valgrind | -fsanitize=memory -fsanitize-memory-track-origins=2 |
| Data races | ThreadSanitizer (not combinable with ASan) | -fsanitize=thread |
| Leaks | LeakSanitizer (part of ASan on Linux), Valgrind, macOS leaks | ASAN_OPTIONS=detect_leaks=1 |
| No recompile possible | Valgrind memcheck | valgrind --leak-check=full --error-exitcode=1 ./prog |
| Bugs before running | GCC -fanalyzer, clang scan-build, clang-tidy clang-analyzer-*, cppcheck | static |
| Unknown inputs | libFuzzer / AFL++ with ASan | -fsanitize=fuzzer,address |
CC=clang CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer -g -O1" \
LDFLAGS="-fsanitize=address,undefined" make clean all check
export ASAN_OPTIONS=abort_on_error=1:halt_on_error=1:detect_stack_use_after_return=1:strict_string_checks=1
export UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
Platform notes: LeakSanitizer is not available with Apple’s toolchain on macOS; use
leaks --atExit -- ./prog there or run the suite in a Linux container. MSan and
Valgrind need Linux; Valgrind does not run on Apple silicon macOS. Installing Valgrind
or pulling a container image needs network access and approval.
Safe patterns
Size arithmetic
#include <stdckdint.h> /* C23; on older compilers use __builtin_mul_overflow */
void *alloc_array(size_t count, size_t elem_size) {
size_t bytes;
if (ckd_mul(&bytes, count, elem_size)) {
errno = EOVERFLOW;
return NULL;
}
return malloc(bytes);
}
calloc(count, size) also checks the multiplication. Never compute malloc(len + 1)
where len comes from input without an upper bound check first.
Strings
| Avoid | Use |
|---|---|
strcpy, strcat, sprintf, gets | snprintf with return-value check; memcpy with an explicit, checked length |
strncpy (may not NUL-terminate, pads) | snprintf(dst, sizeof dst, "%s", src) or strlcpy where available (glibc 2.38+, BSD, macOS) |
printf(user_str) | printf("%s", user_str) |
atoi | strtol with errno, end-pointer and range checks |
int n = snprintf(buf, sizeof buf, "%s/%s", dir, name);
if (n < 0 || (size_t)n >= sizeof buf) {
return ERR_PATH_TOO_LONG; /* truncated: treat as an error, not a success */
}
sizeof buf is only the buffer size when buf is an array in scope; for a pointer
parameter it is the pointer size. Pass lengths explicitly alongside pointers.
Ownership and cleanup
- Document ownership in the header: who allocates, who frees, and whether a returned pointer is borrowed.
- One exit path for cleanup with
goto:
int load(const char *path, struct config *out) {
int rc = -1;
FILE *f = NULL;
char *buf = NULL;
f = fopen(path, "rb");
if (!f) goto done;
buf = malloc(MAX_CONFIG);
if (!buf) goto done;
/* ... parse ... */
rc = 0;
done:
free(buf);
if (f) fclose(f);
return rc;
}
- After
free(p), setp = NULLwhen the pointer remains reachable (struct fields, globals); it turns a later use-after-free into a deterministic NULL crash and makes double free harmless. reallocinto a temporary:tmp = realloc(p, n); if (!tmp) { /* p still valid */ }.- Initialise every local at declaration; for structs use
= {0}.
Integer pitfalls
- Signed overflow is undefined; do size and index math in
size_tand check bounds before converting from signed input. - Comparisons between
intandsize_tconvert the int to unsigned:-1 < sizeof xis false.-Wsign-comparecatches these. - Shifts by >= the width or of negative values are UB; UBSan reports them.
Hardening flags for release builds
-Wall -Wextra -Wformat=2 -Wformat-security -Wconversion -Wsign-conversion -Wshadow
-Wimplicit-fallthrough -Werror=implicit-function-declaration
-D_FORTIFY_SOURCE=3 -O2 # FORTIFY needs optimisation; =3 on GCC 12+/Clang 16+, else =2
-fstack-protector-strong -fstack-clash-protection
-ftrivial-auto-var-init=zero # GCC 12+/Clang 16+: defangs uninitialised stack reads
-fPIE -pie -Wl,-z,relro,-z,now # Linux ELF
Hardening reduces exploitability; it does not replace fixing the bug. Add flags in the build system per target, check each with every CI compiler, and do not ship sanitizer builds to production.
Fuzzing an input parser
#include <stddef.h>
#include <stdint.h>
#include "parser.h"
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
struct message msg;
if (parse_message(data, size, &msg) == 0) {
message_free(&msg);
}
return 0;
}
clang -g -O1 -fsanitize=fuzzer,address,undefined fuzz_parser.c parser.c -o fuzz_parser
mkdir -p corpus && cp tests/fixtures/*.bin corpus/
./fuzz_parser corpus/ -max_total_time=300
Crashes are written as crash-<hash> files; add each as a regression test before fixing.
Verification
- The regression test fails before the fix and passes after, under ASan+UBSan.
- Full test suite clean under ASan+UBSan with
halt_on_error=1. - No new warnings with the flags above.
- Static analyser run on changed files (
gcc -fanalyzerorscan-build make). - Fuzz target (if added) runs for at least a few minutes with no findings.
Report template
## Memory bug: <one-line summary>
**Class:** heap-buffer-overflow | use-after-free | double-free | leak | uninit read | UB
**Trigger:** <input or test> **Introduced:** <commit if known>
**Root cause:** <ownership/bounds/size reasoning, with file:line>
**Fix:** <what changed and why it removes the class of bug>
**Same pattern elsewhere:** <locations fixed or left, with reason>
**Evidence:** sanitizer report before, clean run after, regression test name What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "C Memory Safety"
short_description: "Reproduce under ASan, fix the cause, kill the pattern"
default_prompt: "$c-memory-safety This C program crashes randomly in production, find the memory bug" references/sanitizer-reports.md
# Reading sanitizer and Valgrind reports
Read this when a report is unfamiliar. Always start at the first error: later errors are
often consequences of the first corruption.
## AddressSanitizer anatomy
```text
==4242==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000015 ...
WRITE of size 1 at 0x602000000015 thread T0
#0 0x4f1a2c in copy_name src/user.c:41:17
#1 0x4f1b10 in user_new src/user.c:63:5
0x602000000015 is located 0 bytes after 5-byte region [0x602000000010,0x602000000015)
allocated by thread T0 here:
#0 0x49c0dd in malloc
#1 0x4f1ae4 in user_new src/user.c:60:22
```
Read it in this order:
1. **Error type** (first line) tells the bug class (table below).
2. **READ or WRITE of size N**: a write is more dangerous; N hints at the type
(1 = char, 4 = int, 8 = pointer).
3. **Faulting stack (#0, #1 ...)**: where the bad access happened. Frame #0 in libc
(`memcpy`, `strlen`) means look at frame #1, the caller that passed bad arguments.
4. **Location relative to region**: "0 bytes after 5-byte region" is the classic
off-by-one (missing room for the NUL). "N bytes before" suggests negative indexing.
5. **Allocation stack**: where the block came from. For use-after-free there is also a
**freed by** stack: the bug is the disagreement between that free and the later use.
| Error type | Usual root cause |
|---|---|
| heap-buffer-overflow | wrong size computation, off-by-one, missing NUL byte, unchecked length from input |
| stack-buffer-overflow | fixed-size local array too small, `sizeof` on a pointer |
| global-buffer-overflow | indexing a static table with an unvalidated value |
| heap-use-after-free | pointer kept after `free`, container reallocated (`realloc`) while a pointer into it was held |
| double-free / attempting free on address which was not malloc()-ed | unclear ownership, freeing an interior pointer or a stack address |
| stack-use-after-return | returning a pointer to a local (needs `detect_stack_use_after_return=1`) |
| stack-use-after-scope | pointer to a block-scoped local used after the block |
| alloc-dealloc-mismatch | mixing allocators (C library vs custom pool) |
| SEGV on unknown address 0x000000000000 | NULL dereference; check the error path that should have returned early |
| memcpy-param-overlap | `memcpy` on overlapping regions; use `memmove` |
Symbolisation: if frames show only addresses, ensure `-g` was used and that
`llvm-symbolizer` is on `PATH` (or set `ASAN_SYMBOLIZER_PATH`).
## LeakSanitizer
```text
Direct leak of 64 byte(s) in 1 object(s) allocated from:
#0 malloc
#1 parse_header src/http.c:88
Indirect leak of 128 byte(s) ...
```
- **Direct** leaks have no pointer to them anywhere: fix these.
- **Indirect** leaks are reachable only from leaked blocks; they usually disappear when
the direct leak is fixed.
- Leaks at exit in long-lived global state can be acceptable; suppress precisely with a
suppressions file (`LSAN_OPTIONS=suppressions=lsan.supp`, lines like `leak:init_cache`)
rather than turning detection off.
## UndefinedBehaviorSanitizer
```text
src/math.c:17:14: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
src/buf.c:9:22: runtime error: load of misaligned address 0x... for type 'uint32_t', which requires 4 byte alignment
src/bits.c:5:12: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
```
UBSan reports file:line:column directly. Without `-fno-sanitize-recover=all` it prints
and continues, so tests can pass while UB happened; always build with it or set
`halt_on_error=1`. Misaligned loads from byte buffers are fixed with `memcpy` into a
properly typed local.
## MemorySanitizer
```text
WARNING: MemorySanitizer: use-of-uninitialized-value
#0 in validate src/check.c:30
Uninitialized value was created by a heap allocation
#0 in malloc
#1 in read_record src/io.c:12
```
The warning fires where an uninitialised value affects control flow or is passed to a
syscall, which can be far from where it was created; the "created by" section (needs
`-fsanitize-memory-track-origins=2`) points to the origin. False positives almost
always mean a non-instrumented library (including libc++ or a prebuilt dependency) wrote
the memory.
## ThreadSanitizer
```text
WARNING: ThreadSanitizer: data race (pid=...)
Write of size 8 at 0x... by thread T2:
#0 counter_inc src/stats.c:14
Previous read of size 8 at 0x... by main thread:
#0 counter_get src/stats.c:20
```
Two stacks touching the same address with at least one write and no common lock.
Fix with a mutex, C11 atomics (`atomic_fetch_add`), or by not sharing the data.
## Valgrind memcheck equivalents
| Valgrind message | ASan equivalent |
|---|---|
| Invalid write of size N / Invalid read of size N | heap-buffer-overflow or use-after-free (check "Address ... is N bytes after a block" vs "inside a block of size N free'd") |
| Conditional jump or move depends on uninitialised value(s) | MSan use-of-uninitialized-value (add `--track-origins=yes`) |
| Invalid free() / delete / delete[] | double-free or bad free |
| definitely lost | direct leak |
| indirectly lost | indirect leak |
| still reachable | memory alive at exit; usually not a bug |
Valgrind runs 20-50x slower than native; use small inputs.
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.