Skip to content

fix(qwen36): refuse loudly on a failed encode-buffer realloc - #1588

Merged
JustVugg merged 1 commit into
JustVugg:devfrom
alter:fix/qwen36-bpe-realloc-guard
Sep 21, 2026
Merged

JustVugg merged 1 commit into
JustVugg:devfrom
alter:fix/qwen36-bpe-realloc-guard

Conversation

@alter

@alter alter commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

push_id and bpe_piece in qwen36.c grow their token-id/BPE-symbol buffers with a bare realloc() and no NULL check. On allocation failure the buffer pointer is overwritten with NULL and the very next store ((*ids)[(*n)++]=v; / syms[sc++]=d;) writes through it. Both are reached from encode_text on every inference request's prompt text (serve_one -> encode_text -> push_id/bpe_piece).

qwen38.c already guards this exact pattern in its own push_id/bpe_piece (q38_encode_realloc/q38_encode_oom); qwen36.c never got the equivalent fix. This applies the same OOM-guard idiom already established for st.h in the #798 defect class: checked realloc, fprintf to stderr naming the buffer, exit(1).

Validation

  • make -C c check
  • CUDA changes were tested with make -C c cuda-test (if applicable) -- n/a, no CUDA touched
  • Performance claims include hardware, commands, and repeatable measurements -- n/a, no performance claim made
  • Performance claims include a validated experiment manifest with raw evidence -- n/a

Added tests/test_qwen36_encode_oom.c, which injects a real allocation failure via the shadow-realloc technique already used by tests/test_798_guards.c, and asserts both call sites exit(1) with a message instead of crashing on the NULL write. Verified red (the forked child SIGSEGVs) against the unpatched function before this change, green after.

Compatibility

  • The default CPU build remains dependency-free
  • No model files, generated binaries, or benchmark artifacts are included

Found via a static-analysis pass (cppcheck) against origin/dev, cross-checked by hand against the source and against qwen38.c's existing fix for the identical pattern.

@JustVugg

Copy link
Copy Markdown
Owner

Reviewed, and this one should go in. The unchecked growth reallocs are a real defect on a path fed by the network, the test injects a genuine allocation failure rather than reading the code, it forks so the refusal is observed as an exit code and a message instead of taking the test process with it, and _exit(42) separates "did not refuse" from "crashed". Reusing tests/test_798_guards.c's shadow-realloc seam, and pointing at qwen38.c already guarding the same two functions, is the evidence that makes it reviewable.

It needs a rebase first, and the conflict is small. What landed is #1557 (fix(qwen36): a truncated multibyte tail made the tokenizer read past the prompt), which adds its own test target in the same place as yours, right after tests/test_qwen36_tok_merges. I reproduced the merge locally: c/qwen36.c merges cleanly on its own, and the only conflict is one hunk in c/Makefile.

There is a trap in resolving it that is worth naming, because the obvious resolution is wrong in a way make will not complain about. The conflict markers fall like this:

<<<<<<< HEAD
# A byte-counted serving payload may end mid-character; ...
tests/test_qwen36_tok_truncated$(EXE): tests/test_qwen36_tok_truncated.c ...
=======
# push_id / bpe_piece must refuse loudly, ...
tests/test_qwen36_encode_oom$(EXE): tests/test_qwen36_encode_oom.c ...
>>>>>>> your branch

The recipe line, the $(CC) ... underneath, is shared context BELOW the markers, so keeping both sides leaves test_qwen36_tok_truncated with a prerequisite line and no recipe. Make accepts that silently and the test is simply never built. The resolution is both targets, each with its own copy of the recipe line and a blank line between them:

# A byte-counted serving payload may end mid-character; the pre-tokenizer must
# not read past it.  qwen38 already gates this (tests/test_qwen38_tokenizer.c).
tests/test_qwen36_tok_truncated$(EXE): tests/test_qwen36_tok_truncated.c qwen36.c expert_ffn.h qwen36_tier.h st.h json.h compat.h $(QWEN36_TIER_SRC) $(CUDA_OBJ)
	$(CC) $(QWEN36_CFLAGS) $< $(QWEN36_TIER_SRC) $(CUDA_OBJ) -o $@ $(QWEN36_LDFLAGS)

# push_id / bpe_piece must refuse loudly, not crash, on a failed realloc during growth.
tests/test_qwen36_encode_oom$(EXE): tests/test_qwen36_encode_oom.c qwen36.c expert_ffn.h qwen36_tier.h st.h json.h compat.h $(QWEN36_TIER_SRC) $(CUDA_OBJ)
	$(CC) $(QWEN36_CFLAGS) $< $(QWEN36_TIER_SRC) $(CUDA_OBJ) -o $@ $(QWEN36_LDFLAGS)

(tabs, not spaces, on both recipe lines.)

I applied exactly that against current dev and built and ran both:

make tests/test_qwen36_encode_oom tests/test_qwen36_tok_truncated
./tests/test_qwen36_encode_oom        ok
./tests/test_qwen36_tok_truncated     ok

So after the rebase there is nothing else to change. Once it is pushed the CI needs an approval from us because it is your first PR here, and I will start it.

Edo771977 added a commit to Edo771977/colibri that referenced this pull request Sep 18, 2026
Integrazione dev + JustVugg#1582 auto-tier VRAM + JustVugg#1580 build Windows CUDA + JustVugg#1588 + tetto RAM_GB qwen36
push_id and bpe_piece grow their token-id/BPE-symbol buffers with a bare
realloc() and no NULL check, so an allocation failure under memory pressure
overwrote the buffer pointer with NULL and the very next store wrote through
it. Both are reached from encode_text on every inference request's prompt
text (serve_one -> encode_text -> push_id/bpe_piece).

qwen38.c's push_id/bpe_piece already guard this exact growth pattern
(q38_encode_realloc / q38_encode_oom); qwen36.c never got the equivalent fix.
This mirrors the project's established OOM-guard idiom (checked realloc,
fprintf to stderr, exit(1)) used in st.h for the JustVugg#798 defect class.

Adds tests/test_qwen36_encode_oom.c, which injects a real allocation failure
via the shadow-realloc technique from tests/test_798_guards.c and asserts
both call sites exit(1) with a message instead of crashing on the NULL
write; verified red (SIGSEGV in the forked child) against the unpatched
function before this fix, green after.
@alter
alter force-pushed the fix/qwen36-bpe-realloc-guard branch from 4c43286 to d548042 Compare September 18, 2026 08:56
@JustVugg
JustVugg merged commit 87e7153 into JustVugg:dev Sep 21, 2026
28 checks passed
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