Repository navigation
Qwen's cfg_sane leaves two shapes unbounded: one writes past m->ff, the other divides by zero on x86 #69
Description
cfg_sane() is where a container stops being untrusted input: every dimension that sizes an allocation or bounds a loop is checked there, and #63's Qwen block does that carefully for fifteen of them. Two are missing, and each one is reachable from a manifest.
1. shared_expert_intermediate_size is unbounded, and the shared expert writes past m->ff
m->ff is sized from the routed and dense widths only:
m->ff = (float *)calloc((size_t)2 * (c->dense_inter > c->moe_inter
? c->dense_inter : c->moe_inter), sizeof(float));but qwen_moe_layer batches the shared expert's gate and up projections into it at shared_inter, which comes straight from the manifest and is never compared against either:
{ m->ff, ...shared_expert.gate_proj.weight..., shared_in },
{ m->ff + shared_in, ...shared_expert.up_proj.weight..., shared_in },validate_qwen_tensors does not catch it — it derives the expected shape from the same shared_inter, so a container whose tensors agree with its own manifest passes.
Reproduced by regenerating the synthetic fixture with shared_expert_intermediate_size: 512 over moe_intermediate_size: 16 and the three shared_expert.* tensors shaped from the former rather than the latter. Under a plain build it loads, runs and prints logits. Under ASan:
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 4 at 0x60c000000780 ... in waste_mvq4_rows_i8mm simd_i8mm.c:73
#1 mvb_pieces model.c:1034
#2 waste__worker threads.h:197
0 bytes after 128-byte region [0x60c000000700,0x60c000000780)
allocated by thread T0 in waste_model_load model.c:3272
A write, not a read, and off the calling thread. It survives on Flash-Next only by coincidence: shared_expert_intermediate_size and moe_intermediate_size are both 640 there, so the buffer fits exactly.
Either bound it — shared_inter <= max(moe_inter, dense_inter) — or size m->ff from shared_inter as well. The bound is the better half of the pair: it says which containers this engine will run, which is what cfg_sane is for.
2. ngram_size may be 1, and validate_qwen_tensors divides by ngram_size - 1
const int width = (c->ple_embed && c->heads_per_ngram)
? c->ple_embed / ((c->ngram_size - 1) * c->heads_per_ngram) : 8;cfg_sane admits ngram_size >= 1, and the guard in front of the division tests the two factors that are not zero. With ngram_size: 1 and a PLE layer the divisor is zero.
This is the same failure the file already documents a few hundred lines further down, where group was 0 on a skipped tensor:
arm64's sdiv quietly yields 0, x86's idiv raises #DE, so
waste infoon K3 was an instant SIGFPE on every x86 build while every check here stayed green. issue #10
Same split here. Editing the fixture's manifest to ngram_size: 1 on an arm64 build gives width == 0 and a refusal for the wrong reason ("required tensor is missing or has the wrong shape"); compiling the expression for both ISAs gives rc=0 on arm64 and rc=136 — SIGFPE — on x86_64. Every x86 CI job would be green and every x86 user would get a crash.
Requiring ngram_size >= 2 when ple_layer >= 0 covers it, and it is what the PLE path actually needs: ngram_size: 1 produces zero heads and an all-zero embedding anyway.
3. Same family, smaller: linear_conv_kernel_dim is not required to be >= 1 for Qwen
The conv_k < 1 test in cfg_sane sits on the non-Qwen branch. With KS == 0, gdn_conv_range computes ring + b * (KS - 1) — a negative offset — and REQUIRE_DATA(conv1d.weight, qkv * 0) asks for nothing, so the container is accepted.
Checks
tests/run.sh already has the right shape for this: five qwen_refused cases, each a one-line manifest edit. Three more entries there — an oversized shared expert, ngram_size: 1, linear_conv_kernel_dim: 0 — would pin all three, and the first is worth running under make asan too, since a plain build does not notice it.
Found reviewing #63; the code is on feature/qwen38-flash-next and not yet on main. See also #68, from the same review.
Activity
marcobambini commentedon Sep 18, 2026
Fixed by #63, merged in ae0b1b0. All three are closed, but the first one not in the way this issue proposed.
3b1c4f6. This issue suggested boundingshared_expert_intermediate_sizebymax(moe, dense), and that bound turned out to be wrong twice over. It refuses a real shape: Qwen2-57B-A14B's shared expert is wider than both of its other widths. And it did not close the overflow:m->xq, which quantizes the down projection's input, is not sized frommoe, somoe = shared = 2048over a 32-wide hidden state passed the bound and still wrote past it. So the width is now bounded like its siblings (1..2^20), andm->ff,m->xqandm->xsare all sized from it, withwaste_plan_memoryplanning the same bytes.ngram_size: 1— refused bycfg_sanewhen there is a PLE layer (01e593c). UBSan confirmed the division atmodel.c:1542was reachable.linear_conv_kernel_dim: 0— refused bycfg_sane(01e593c).These are pinned by the checks this issue asked for, with one change made along the way.
qwen_refusedused to count any non-zero exit as a refusal, including a crash. It now requirescfg_sane's message and an exit status that is not a signal. A shared expert 2048 wide — built by the newmake_test_container.py --qwen-shared N— opens, runs undermake asan, and matches the container-native oracle. Each of these checks fails on a tree that lacks its fix; the table is in the #63 thread.