Skip to content

Reject spoofed audio markers and reserved tokens in transcripts - #116

Merged
aviv1ron1 merged 2 commits into
asrfrom
bugfix/audio-marker-count-validation
Aug 9, 2026
Merged

Reject spoofed audio markers and reserved tokens in transcripts#116
aviv1ron1 merged 2 commits into
asrfrom
bugfix/audio-marker-count-validation

Conversation

@aviv1ron1

Copy link
Copy Markdown
Collaborator

Addresses review findings #3 (P0, marker spoofable from user text) and #16 (P2, transcript tokenized with special tokens parsed) — the same defect in opposite directions.

<|audio|> is a registered special token, so text a caller types is tokenized into the real marker. vLLM pairs markers with audio items positionally and stops once every item is matched, appending the rest of the prompt verbatim, so an extra marker is not an error: the leftmost one wins the transcript and vLLM's own _validate_mm_placeholders still sees matching counts and passes. A caller could therefore move the transcript to a position of their choosing, and in a multi-clip request shift every transcript onto the wrong clip, with no error raised. The reverse case — markers with no audio payload — is the vector behind vLLM's own CVE-2026-44222 (GHSA-hpv8-x276-m59f), where models indexing a grid from a spoofed placeholder hit an unhandled IndexError.

Inbound, encode(..., add_special_tokens=False) only suppresses added BOS and EOS; special-token strings already present in the text are still parsed into genuine ids. Since the switch sets requires_raw_input_tokens and detects adapters on raw input_ids, a control token arriving via the transcript would steer adapter selection from audio content.

Two guards:

  • apply() requires the marker count to equal the audio item count, for both str and token-id prompts. Enforced there rather than in _call_hf_processor because that runs with only the cache-missing items, so on a processor cache hit its item count is smaller than the request's and the comparison would be wrong. apply() is the only entry point that always sees the whole request. Its parameters differ across the vLLM versions this package supports, so both shapes are read defensively; an unrecognised signature raises rather than skipping the check, since a security control that silently no-ops is worse than none.

  • _transcribe refuses a transcript that tokenized into the audio marker or any adapter control token. Rejected rather than neutralized with split_special_tokens=True so the condition is visible instead of silently rewriting model output: such a transcript means either an attack or a badly misbehaving ASR backend, and both are worth surfacing.

Consequence worth knowing: a text-only prompt containing <|audio|> is now refused. That is the CVE vector, but it also rejects legitimate prose that mentions the marker — the accepted cost of count-based rather than provenance-based validation. Stripping the marker from user content at chat template render time is the only fix for that, and it waits on finding #15 making the template injection robust and tested.

Tests: 13 new cases covering injection from text, from token ids, with no audio payload at all, and from the transcript (both the marker and an adapter control token), plus negative controls so that rejecting everything would not pass. Verified as genuine guards by reverting each fix in turn: 3 fail without the apply() check, 2 without the transcript guard. The tokenizer fakes in the test module gained convert_tokens_to_ids, which every real tokenizer has and the new guard needs.

The apply() signature handling was verified against an installed vLLM 0.21 only; 0.19/0.20 behaviour needs the GPU run to confirm.

Addresses review findings #3 (P0, marker spoofable from user text) and #16
(P2, transcript tokenized with special tokens parsed) — the same defect in
opposite directions.

<|audio|> is a registered special token, so text a caller types is tokenized
into the *real* marker. vLLM pairs markers with audio items positionally and
stops once every item is matched, appending the rest of the prompt verbatim,
so an extra marker is not an error: the leftmost one wins the transcript and
vLLM's own _validate_mm_placeholders still sees matching counts and passes.
A caller could therefore move the transcript to a position of their choosing,
and in a multi-clip request shift every transcript onto the wrong clip, with
no error raised. The reverse case — markers with no audio payload — is the
vector behind vLLM's own CVE-2026-44222 (GHSA-hpv8-x276-m59f), where models
indexing a grid from a spoofed placeholder hit an unhandled IndexError.

Inbound, encode(..., add_special_tokens=False) only suppresses *added* BOS and
EOS; special-token strings already present in the text are still parsed into
genuine ids. Since the switch sets requires_raw_input_tokens and detects
adapters on raw input_ids, a control token arriving via the transcript would
steer adapter selection from audio content.

Two guards:

- apply() requires the marker count to equal the audio item count, for both
  str and token-id prompts. Enforced there rather than in _call_hf_processor
  because that runs with only the cache-*missing* items, so on a processor
  cache hit its item count is smaller than the request's and the comparison
  would be wrong. apply() is the only entry point that always sees the whole
  request. Its parameters differ across the vLLM versions this package
  supports, so both shapes are read defensively; an unrecognised signature
  raises rather than skipping the check, since a security control that
  silently no-ops is worse than none.

- _transcribe refuses a transcript that tokenized into the audio marker or any
  adapter control token. Rejected rather than neutralized with
  split_special_tokens=True so the condition is visible instead of silently
  rewriting model output: such a transcript means either an attack or a badly
  misbehaving ASR backend, and both are worth surfacing.

Consequence worth knowing: a text-only prompt containing <|audio|> is now
refused. That is the CVE vector, but it also rejects legitimate prose that
mentions the marker — the accepted cost of count-based rather than
provenance-based validation. Stripping the marker from user content at chat
template render time is the only fix for that, and it waits on finding #15
making the template injection robust and tested.

Tests: 13 new cases covering injection from text, from token ids, with no
audio payload at all, and from the transcript (both the marker and an adapter
control token), plus negative controls so that rejecting everything would not
pass. Verified as genuine guards by reverting each fix in turn: 3 fail without
the apply() check, 2 without the transcript guard. The tokenizer fakes in the
test module gained convert_tokens_to_ids, which every real tokenizer has and
the new guard needs.

The apply() signature handling was verified against an installed vLLM 0.21
only; 0.19/0.20 behaviour needs the GPU run to confirm.

Signed-off-by: aviv ron <rona@il.ibm.com>
@aviv1ron1
aviv1ron1 changed the base branch from asr to main August 9, 2026 14:58
GitHub did not schedule a workflow run for the pull_request opened event on
3fcbecf, so no CI or DCO check suite was created. This empty commit fires a
synchronize event to schedule them.

Signed-off-by: aviv ron <rona@il.ibm.com>
@aviv1ron1

Copy link
Copy Markdown
Collaborator Author

/gpu-test

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

❌ GPU tests failed — vllm19

2 failed, 1498 passed, 19 skipped, 20 warnings in 6370.31s (1:46:10)

Commit: 6117236174838776d17051ec7a4653d5116c369c
Full run & artifact log

Last 40 log lines
E         hf_argmax   = [279, 334, 315, 2010, 304, 279, 2010]
E         vllm_argmax = [279, 334, 315, 3575, 304, 279, 2010]
E       This typically indicates a vLLM gain-compensation bug: the switch
E       produced wrong adapter_indices, the wrong LoRA was applied, and
E       the downstream logits diverged enough to flip the top token.
E   assert not [('early', [3], [279, 334, 315, 2010, 304, 279, ...], [279, 334, 315, 3575, 304, 279, ...])]
=============================== warnings summary ===============================
.venv/lib/python3.12/site-packages/torch/jit/_script.py:362: 14 warnings
  /tmp/granite-switch/.venv/lib/python3.12/site-packages/torch/jit/_script.py:362: DeprecationWarning: `torch.jit.script_method` is deprecated. Please switch to `torch.compile` or `torch.export`.
    warnings.warn(

<frozen importlib._bootstrap>:488
  <frozen importlib._bootstrap>:488: DeprecationWarning: builtin type SwigPyPacked has no __module__ attribute

<frozen importlib._bootstrap>:488
  <frozen importlib._bootstrap>:488: DeprecationWarning: builtin type SwigPyObject has no __module__ attribute

tests/composer/test_compose_e2e.py:83
  /tmp/granite-switch/tests/composer/test_compose_e2e.py:83: PytestUnknownMarkWarning: Unknown pytest.mark.xdist_group - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
    @pytest.mark.xdist_group("compose_e2e")

tests/composer/test_upstream_files.py:218
  /tmp/granite-switch/tests/composer/test_upstream_files.py:218: PytestUnknownMarkWarning: Unknown pytest.mark.xdist_group - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
    @pytest.mark.xdist_group("upstream_build_e2e")

tests/integration/test_hf_to_vllm_weights.py::TestSingleSwitchForwardEquivalence::test_forward_logit_equivalence
  /tmp/granite-switch/.venv/lib/python3.12/site-packages/flashinfer/gemm/kernels/grouped_gemm_masked_blackwell.py:2059: DeprecationWarning: tcgen05.OperandMajorMode is deprecated, use cute.nvgpu.OperandMajorMode instead
    a_major_mode: tcgen05.OperandMajorMode,

tests/integration/test_hf_to_vllm_weights.py::TestSingleSwitchForwardEquivalence::test_forward_logit_equivalence
  /tmp/granite-switch/.venv/lib/python3.12/site-packages/flashinfer/gemm/kernels/grouped_gemm_masked_blackwell.py:2061: DeprecationWarning: tcgen05.OperandMajorMode is deprecated, use cute.nvgpu.OperandMajorMode instead
    b_major_mode: tcgen05.OperandMajorMode,

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/integration/test_switch_e2e_compose.py::test_hf_vllm_argmax_equivalence[granite-4.0-micro]
FAILED tests/integration/test_switch_e2e_compose.py::test_hf_vllm_argmax_equivalence[granite-4.1-3b]
===== 2 failed, 1498 passed, 19 skipped, 20 warnings in 6370.31s (1:46:10) =====

Verdict: FAILED — 2 failed, 1498 passed, 19 skipped, 20 warnings in 6370.31s (1:46:10)

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

❌ GPU tests failed — vllm20

2 failed, 1498 passed, 19 skipped, 22 warnings in 7564.42s (2:06:04)

Commit: 6117236174838776d17051ec7a4653d5116c369c
Full run & artifact log

Last 40 log lines
E   assert not [('early', [3], [279, 334, 315, 2010, 304, 279, ...], [279, 334, 315, 3575, 304, 279, ...])]
=============================== warnings summary ===============================
.venv/lib/python3.12/site-packages/torch/jit/_script.py:365: 14 warnings
  /tmp/granite-switch/.venv/lib/python3.12/site-packages/torch/jit/_script.py:365: DeprecationWarning: `torch.jit.script_method` is deprecated. Please switch to `torch.compile` or `torch.export`.
    warnings.warn(

<frozen importlib._bootstrap>:488
  <frozen importlib._bootstrap>:488: DeprecationWarning: builtin type SwigPyPacked has no __module__ attribute

<frozen importlib._bootstrap>:488
  <frozen importlib._bootstrap>:488: DeprecationWarning: builtin type SwigPyObject has no __module__ attribute

tests/composer/test_compose_e2e.py:83
  /tmp/granite-switch/tests/composer/test_compose_e2e.py:83: PytestUnknownMarkWarning: Unknown pytest.mark.xdist_group - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
    @pytest.mark.xdist_group("compose_e2e")

tests/composer/test_upstream_files.py:218
  /tmp/granite-switch/tests/composer/test_upstream_files.py:218: PytestUnknownMarkWarning: Unknown pytest.mark.xdist_group - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
    @pytest.mark.xdist_group("upstream_build_e2e")

tests/integration/test_hf_to_vllm_weights.py::TestSingleSwitchForwardEquivalence::test_forward_logit_equivalence
  /tmp/granite-switch/.venv/lib/python3.12/site-packages/flashinfer/gemm/kernels/grouped_gemm_masked_blackwell.py:2059: DeprecationWarning: tcgen05.OperandMajorMode is deprecated, use cute.nvgpu.OperandMajorMode instead
    a_major_mode: tcgen05.OperandMajorMode,

tests/integration/test_hf_to_vllm_weights.py::TestSingleSwitchForwardEquivalence::test_forward_logit_equivalence
  /tmp/granite-switch/.venv/lib/python3.12/site-packages/flashinfer/gemm/kernels/grouped_gemm_masked_blackwell.py:2061: DeprecationWarning: tcgen05.OperandMajorMode is deprecated, use cute.nvgpu.OperandMajorMode instead
    b_major_mode: tcgen05.OperandMajorMode,

tests/integration/test_hf_to_vllm_weights.py::TestSingleSwitchForwardEquivalence::test_forward_logit_equivalence
tests/integration/test_hf_to_vllm_weights.py::TestSingleSwitchForwardEquivalence::test_forward_logit_equivalence
  /tmp/granite-switch/.venv/lib/python3.12/site-packages/flashinfer/gdn_kernels/blackwell/gated_delta_net_chunked.py:99: DeprecationWarning: tcgen05.OperandMajorMode is deprecated, use cute.nvgpu.OperandMajorMode instead
    from cutlass.cute.nvgpu.tcgen05 import OperandMajorMode

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/vllm/test_granite4_fullsize.py::TestGranite4FullSizeEquivalence::test_suite
FAILED tests/integration/test_switch_e2e_compose.py::test_hf_vllm_argmax_equivalence[granite-4.1-3b]
===== 2 failed, 1498 passed, 19 skipped, 22 warnings in 7564.42s (2:06:04) =====

Verdict: FAILED — 2 failed, 1498 passed, 19 skipped, 22 warnings in 7564.42s (2:06:04)

@aviv1ron1
aviv1ron1 changed the base branch from main to asr August 9, 2026 18:01
@aviv1ron1
aviv1ron1 marked this pull request as ready for review August 9, 2026 18:01
@aviv1ron1
aviv1ron1 merged commit de96894 into asr Aug 9, 2026
4 of 6 checks passed
@aviv1ron1
aviv1ron1 deleted the bugfix/audio-marker-count-validation branch August 9, 2026 18:01
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