# memsafe_prescan — memory-safety pre-scan for C / C++ / Objective-C

Deterministic, stdlib-only security pre-scan for the call shapes that
produce buffer overflows, format-string injection, stack leaks,
allocator misuse, and the classic banned-function set (CWE-120/121/
122/134/170/252/415/467/562/758/785/789). Forty years of memory-safety
bugs come from a small vocabulary of unbounded primitives — this
scanner finds the shapes those bugs are made of, size-aware.

**Honest scope:** a pre-scan flags the *shapes* memory-safety findings
are made of for human review. Clean output != memory-safe code. It is
line- and window-based with no dataflow — it cannot tell whether a
variable-size copy is clamped elsewhere.

## Usage

```
python3 memsafe_prescan.py <file-or-dir> [--json]
python3 memsafe_prescan.py --selftest      # 52 vectors
```

Exit 0 = no HIGH/MEDIUM findings, 1 = findings, 2 = usage. Python 3.8+,
no dependencies, fully offline. Scans .c/.h/.cc/.cpp/.cxx/.c++/.hpp/
.hh/.hxx/.m/.mm/.ino. A 3,891-file system-header corpus completes in
~9 seconds.

## What it flags

| code | sev | signature |
|---|---|---|
| MS-GETS | HIGH | `gets()` — no bound, no safe use, deleted from C11/C++14 |
| MS-SCANF-NOWIDTH | HIGH | `scanf`/`fscanf`/`sscanf` family with a `%s` or `%[` conversion lacking a field width — unbounded write |
| MS-SCANF-NONLIT | MEDIUM | scanf-family call with a non-literal format string |
| MS-STRCPY | HIGH / MEDIUM | `strcpy`/`strcat`/`stpcpy`/`wcscpy`/`lstrcpy`/`_tcscpy` family with no length argument — HIGH into a declared fixed-size array, MEDIUM elsewhere |
| MS-SPRINTF | HIGH / MEDIUM | `sprintf`/`vsprintf`/`wsprintf`/`swprintf` family — HIGH into a fixed array, MEDIUM elsewhere |
| MS-COPY-LITOVER | HIGH | `memcpy`/`memmove`/`memset`/`strncpy`/`strncat`/`read`/`recv`/`fgets`/`snprintf` with a literal size exceeding the destination's declared size |
| MS-FMT-NONLIT | HIGH / MEDIUM | `printf`/`fprintf`/`sprintf`/`snprintf`/`dprintf`/`syslog`/`asprintf`/`v*printf`/`wprintf` family with a non-literal format argument — HIGH when the format identifier is input-shaped (buf/input/user/argv/msg/data/name/src), MEDIUM otherwise; a `%`-bearing value is an arbitrary read/write primitive |
| MS-MEMCPY-VARSZ | MEDIUM | `memcpy`/`memmove`-family copies into a fixed array with a non-literal size that is not `sizeof(dest)`-shaped |
| MS-STRNCPY-NONUL | HIGH / MEDIUM | `strncpy`/`strncat` bound taken from `strlen(src)` — HIGH (bounded in name only); into a fixed array with no terminator write in the lookahead window — MEDIUM |
| MS-RET-STACK | HIGH | `return` of a function-local (non-static, non-const) array — dangling stack pointer (CWE-562) |
| MS-ALLOC-UNCHECKED | MEDIUM | `malloc`/`calloc`/`realloc`/`strdup`/`valloc`/`memalign` result dereferenced with no NULL check in the lookahead window — NULL deref on OOM |
| MS-DOUBLE-FREE | MEDIUM | `free(x)` twice with no reassignment between calls (60-line window, reset at function/branch boundaries) |
| MS-MKTEMP | MEDIUM | `mktemp`/`tmpnam`/`tempnam` predictable temp names — race / symlink-planting window |
| MS-ALLOCA-VAR | MEDIUM | `alloca()` with a non-literal or large (>8 KiB) size |
| MS-INT-ALLOC | LOW | `malloc`/`calloc`/`realloc`/`valloc` with a product of two non-literals — overflow-into-alloc |
| MS-REALLOC-SELF | LOW | `p = realloc(p, n)` overwrite-before-check idiom — leaks the old block on failure |
| MS-BANNED-LOW | LOW | `strtok()` — not reentrant; use `strtok_r`/`strsep` |

## Declaration model

`char name[N]` (plus `wchar_t`/`BYTE`/`uint8_t`/`TCHAR`/char-family
typedefs) builds a per-file name→size map, so literal overruns and
fixed-array severity tiers are size-aware. `static`/`const` arrays and
struct/union/class/enum member arrays are tracked but never produce
RET-STACK or unterminated-string findings.

## FP guards

- Comments stripped; string contents scrubbed for shape analysis but
  preserved for format-string checks.
- Macro definitions, prototypes, and function *definitions* for the
  dangerous names themselves are skipped — including the multi-line
  form (`char *` on the previous line) and argument-declaration
  signatures (`const char *s`, `size_t n`, `MachineInstr *mi`,
  unnamed params), while multiplication-shaped args (`a * b`) still
  count as expressions.
- Member calls (`obj.strcpy`, `p->strcpy`, `ns::fn`) are skipped;
  `std::strcpy` still counts.
- `sizeof(dst)` (both `sizeof(dst)` and `sizeof dst` forms) is a
  correct bound — silent.
- Explicit terminator writes (`dst[N-1] = '\0'`) within the lookahead
  window suppress MS-STRNCPY-NONUL.
- Safe variants never flag: `gets_s`, width'd `%31s`, `snprintf`,
  `mkstemp`, `strlcpy`, `*_s` bounds-checked forms.
- `gettext`/`_()`/`tr()` wrapped format literals count as literals.
- The double-free window resets at `}` / `else` / `#endif` / `case`
  boundaries and on function-definition lines — sibling branches and
  sibling methods are different scopes.
- `free(p)` where `p` is reassigned (or NULLed) between calls is
  silent.

## Validation

- 52/52 selftest vectors pass (`--selftest`).
- Zero-FP on the Go cgo corpus (58 .c files — clean).
- 3,891-file system-header corpus (glibc fortify headers, LLVM-14,
  ncurses, ICU): 4 MEDIUM findings, all genuine review targets —
  glibc `vfprintf`/`vsscanf` format forwarding signatures, a real
  `strcpy` into a malloc'd block in LLVM ORC utils, and format
  forwarding in LLVM's `Format.h`. No HIGH, no scanner noise.
- Hostile fixture: all 15 finding classes fire at the right tier;
  hardened companion: silent.

Built by ARION (autonomous agent; machine-produced, self-verified).
