security: harden safetensors/JSON parsers against malicious model files

A downloaded (supply-chain) model file was fully trusted by the loader. Three
memory-safety holes, all reachable by pointing the engine at a crafted shard —
demonstrated crashing on pre-fix, now rejected fail-closed:

st.h (safetensors):
- header length `hlen` (u64 from the file) was unbounded before malloc(hlen+1):
  a crafted value overflows (malloc(0) then hdr[hlen]=0 OOB) or forces a giant
  allocation. Now bounded to the file size and a 512 MB cap; malloc NULL-checked.
- json_get() returns NULL for missing/mistyped fields, but dtype/data_offsets/
  shape were dereferenced blind (off->kids[0]) — a header omitting data_offsets
  SIGSEGV'd (verified). Now type/arity-checked before use.
- data_offsets [a0,b0] were trusted: b0<a0 gave a negative nbytes -> malloc((size_t))
  giant and an oversized memcpy into the caller's buffer in st_read_f32 (heap
  overflow); off could point outside the file. Now validated 0<=a0<=b0 and
  data_start+b0<=filesize.

json.h: j_parse_val recursed with no depth limit -> stack overflow on nested
input like [[[[...]]]]. Added J_MAX_DEPTH=1024 (headers are ~3 deep); wide-but-
flat objects like the GLM header are unaffected (depth is decremented per return).

eval_glm.py: tempfile.mktemp() -> mkstemp() — closes the TOCTOU/symlink race on
a shared tmp dir (CWE-377).

Network path (openai_server.py + serve SUBMIT parser) audited separately and is
already sound: hmac.compare_digest auth, MAX_BODY cap, resolve()+relative_to
traversal guard, list-form subprocess, bounded/validated SUBMIT header. All 62
tests pass; valid GLM/OLMoE shards load unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
JustVugg
2026-07-15 08:13:20 +02:00
parent a8895f2d84
commit 4268e00fa1
3 changed files with 52 additions and 5 deletions
+13 -3
View File
@@ -26,8 +26,14 @@ typedef struct {
const char *s;
char *arena; /* buffer per le stringhe smontate */
size_t acap, aoff;
int depth; /* annidamento corrente: bound contro lo stack-overflow
* da JSON malevolo tipo [[[[...]]]] (discesa ricorsiva) */
} jparser;
/* tetto di annidamento: gli header safetensors / config sono piatti (profondita'
* ~3). 1024 e' larghissimo per input legittimi e ben sotto il limite di stack. */
#define J_MAX_DEPTH 1024
static char *j_dup(jparser *p, const char *b, int n) {
/* ogni stringa ha la sua allocazione: un'arena con realloc sposterebbe il
* buffer invalidando i puntatori gia' emessi (use-after-free). */
@@ -91,10 +97,11 @@ static jval *j_parse_val(jparser *p) {
char c = *p->s;
if (c == '"') { jval *v = j_new(J_STR); v->str = j_parse_str_raw(p); return v; }
if (c == '{') {
if (++p->depth > J_MAX_DEPTH) { p->depth--; return j_new(J_NULL); }
p->s++; jval *v = j_new(J_OBJ);
int cap = 8; v->keys = malloc(cap * sizeof(char*)); v->kids = malloc(cap * sizeof(jval*));
j_ws(p);
if (*p->s == '}') { p->s++; return v; }
if (*p->s == '}') { p->s++; p->depth--; return v; }
for (;;) {
j_ws(p);
char *key = j_parse_str_raw(p);
@@ -107,13 +114,15 @@ static jval *j_parse_val(jparser *p) {
if (*p->s == '}') { p->s++; break; }
break;
}
p->depth--;
return v;
}
if (c == '[') {
if (++p->depth > J_MAX_DEPTH) { p->depth--; return j_new(J_NULL); }
p->s++; jval *v = j_new(J_ARR);
int cap = 8; v->kids = malloc(cap * sizeof(jval*));
j_ws(p);
if (*p->s == ']') { p->s++; return v; }
if (*p->s == ']') { p->s++; p->depth--; return v; }
for (;;) {
jval *val = j_parse_val(p);
if (v->len == cap) { cap *= 2; v->kids = realloc(v->kids, cap*sizeof(jval*)); }
@@ -123,6 +132,7 @@ static jval *j_parse_val(jparser *p) {
if (*p->s == ']') { p->s++; break; }
break;
}
p->depth--;
return v;
}
if (c == 't') { p->s += 4; jval *v = j_new(J_BOOL); v->boolean = 1; return v; }
@@ -134,7 +144,7 @@ static jval *j_parse_val(jparser *p) {
/* API */
static jval *json_parse(const char *text, char **arena_out) {
jparser p = { text, NULL, 0, 0 };
jparser p = { text, NULL, 0, 0, 0 };
jval *v = j_parse_val(&p);
if (arena_out) *arena_out = p.arena; else free(p.arena);
return v;