Skip to content

Qwen's cfg_sane leaves two shapes unbounded: one writes past m->ff, the other divides by zero on x86 #69

Description

@marcobambini

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 info on 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions