Skip to content

fix(c): check alloc/snprintf results on the checkpoint-load path (#798) - #1101

Merged
JustVugg merged 1 commit into
JustVugg:devfrom
monotophic:fix/798-loader-guards
Aug 19, 2026
Merged

fix(c): check alloc/snprintf results on the checkpoint-load path (#798)#1101
JustVugg merged 1 commit into
JustVugg:devfrom
monotophic:fix/798-loader-guards

Conversation

@monotophic

Copy link
Copy Markdown
Contributor

Authored by Fable 5 in Claude Code, analysis in partnership with @monotophic

This implements the checkpoint-load-path defect set surfaced in #798
credit for the find belongs to @ZacharyZcR (per our comment there: we'd
be glad to see the broader #798 work land with their authorship; this PR
covers only the load-path correctness subset, independently re-derived).

Unchecked realloc/strdup results in c/st.h and c/json.h NULL-store
on allocation failure mid-load (SIGSEGV or corrupted state), and
c/route_trace.h's stats-save builds its temp path with an unchecked
snprintf — a too-long or encoding-failed path would silently write to a
DIFFERENT file than asked. All sites on the load path now fail cleanly
with named diagnostics; a truncated or failed path refuses instead of
landing elsewhere.

Scope, stated plainly: the #798-named sites on the checkpoint-load
path. Other unchecked allocation sites exist in these files and are
deliberately out of scope here.

Behavioral contract

  • Allocation failure during checkpoint load produces a clean named
    failure, never a NULL-store/SIGSEGV, never partial silent state.
  • Stats-save path building refuses on truncation OR negative snprintf
    return, before any filesystem touch; nothing is written to an
    unintended path.
  • No behavior change on the success path (existing suite unchanged).

Capstone matrix

claim decisive evidence
the defects are real injection on unfixed base: 20/20 assertions fail incl. SIGSEGV (exit 139) at the j_parse_val initial-malloc sites
each guard covers its own site mutation matrix: 8 per-site un-guardings each killed by exactly the matching subtest, no cross-fire
truncation guard is observable, boundary-exact refusal at path length 2096 exactly, none at 2095, before filesystem touch; guard-removed mutant caught by stderr assertion
nothing regresses full make test-c exit 0 (ALL PASS); make check + METAL suites green on final head
Fuller matrix and review record Two-reviewer roster (blind validator + deep auditor). Review changes, landed with produce-first evidence: (1) `j_parse_val`'s initial keys/kids allocations were still unguarded while later ones were — reproduced by injection (SIGSEGV), then guarded per file convention; (2) negative `snprintf` return now takes the same refusal path (seam-injected -1 test); (3) commit prose rescoped from all-sites to the #798 sites. Sibling unchecked sites recorded out-of-scope: st.h:147,468,565,584; json.h:41,49 (line refs at ad79236). Windows lane compiles out the fork-based injection tests (existing repo idiom); guards themselves are platform-independent.

Durable vs current-state: guards are durable; sibling line references
are current-state (base ad79236, 2026-08-18).

…-load path

Reported by ZacharyZcR in issue JustVugg#798 (the unchecked reallocs and the ignored
snprintf return); all credit for finding those goes to ZacharyZcR. The strdup
pair and the json.h initial-malloc guards are our own additions on the same
path.

Scope: the sites enumerated below only -- NOT every allocation site on the
checkpoint-load path; known unguarded siblings elsewhere in st.h and json.h
are deliberately left to their own change.

Paths that ignored a fallible call's result:

  st.h            the colibri.fmt stamp table growth (fmt_name/fmt_val)
                  assigned realloc's result straight back over the original
                  pointer, and the two stamp strings' strdup results were
                  stored unchecked -- a dropped fmt_val is worse than a
                  crash: st_fmt_stamp() would return NULL for a tensor that
                  IS stamped, silently disabling qt_verify_fmt_stamp's
                  format check for that tensor.
  json.h          j_parse_val's object (keys/kids) and array (kids) growth
                  assigned realloc's result straight back; on failure the
                  very next statement stores through the NULL. The same
                  function's initial cap=8 keys/kids mallocs had the same
                  defect and get the same guard.
  route_trace.h   rt_save ignored snprintf's return building "<path>.tmp":
                  an overlong path would silently write-then-rename to a
                  TRUNCATED path instead of the caller's, and a negative
                  (encoding-error) return leaves the buffer indeterminate,
                  so proceeding would fopen whatever bytes happen to be
                  there. Both refuse through the same path.

tests/test_798_guards.c exercises every guard above with real failure
injection (malloc/realloc/strdup/snprintf shadowed to fail on a specific,
documented call ordinal, every other call passed through), plus controls, and
asserts each refusal's own diagnostic in a forked child so the refusal is
provably the guard's and not a downstream failure on the bad value.
Registered as a Makefile test gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants