{"schema":"threatline.record/2","record":{"id":"GHSA-rwfw-xvg3-3937","aliases":["GHSA-rwfw-xvg3-3937"],"summary":"Rust gRPC CandidateTokens.top_n bypasses max_logprobs and can terminate EngineCore","details":"## Summary\n\nThe Rust gRPC `Generate` and `GenerateStream` API accepts `CandidateTokens.top_n` from `ResponseOptions.output_candidates` and `ResponseOptions.prompt_candidates`, casts the protobuf `uint32` value to signed `i32`, and lowers it directly into `SamplingParams.logprobs` or `SamplingParams.prompt_logprobs`.\n\nPython `SamplingParams` rejects negative logprob counts other than `-1`, rejects sample and prompt logprob counts above `model_config.max_logprobs`, and defaults `max_logprobs` to 20. The official CLI docs warn that removing the cap with `--max-logprobs -1` allows `output_length * vocab_size` logprobs and may cause OOM.\n\nRust gRPC bypasses that intended guard. A caller can request an oversized `top_n` such as `1_000_000`, causing the lower sampler path to call `torch.topk(logits, num_logprobs)` with a request-controlled count that Python would have rejected. A caller can also send `top_n = 2_147_483_648`; Rust casts that `uint32` to `-2_147_483_648`, bypassing the Rust converter and reaching the lower `assert num_logprobs >= 0` / top-k path as an invalid negative count.\n\n## Technical Details\n\nThe protobuf exposes an unbounded `uint32 top_n` selector for both output and prompt candidate logprobs:\n\n```proto\nmessage ResponseOptions {\n  bool prompt_logprobs = 2;\n  optional CandidateTokens prompt_candidates = 3;\n  bool output_logprobs = 6;\n  optional CandidateTokens output_candidates = 7;\n}\n\nmessage CandidateTokens {\n  oneof select {\n    uint32 top_n = 1;\n    TokenIds token_ids = 2;\n    bool all = 3;\n  }\n}\n```\n\nThe Rust gRPC converter lowers `top_n` without checking the configured `max_logprobs` cap and without checking the `uint32` to `i32` conversion:\n\n```rust\nif r.output_logprobs {\n    let (count, token_ids) = candidate_logprob_spec(r.output_candidates.as_ref());\n    params.logprobs = Some(count);\n    params.logprob_token_ids = token_ids;\n}\n\nif r.prompt_logprobs {\n    let (count, _) = candidate_logprob_spec(r.prompt_candidates.as_ref());\n    params.prompt_logprobs = Some(count);\n}\n\nSome(pb::candidate_tokens::Select::TopN(n)) => (*n as i32, None),\n```\n\nThe Rust text lowerer forwards those fields to engine-core without validation:\n\n```rust\nEngineCoreSamplingParams {\n    logprobs,\n    prompt_logprobs,\n    ...\n}\n```\n\nBy contrast, Python validates the same fields:\n\n```python\nif self.logprobs is not None and self.logprobs != -1 and self.logprobs < 0:\n    raise VLLMValidationError(...)\n\nif num_logprobs := self.logprobs:\n    if num_logprobs == -1:\n        num_logprobs = model_config.get_vocab_size()\n    if num_logprobs > max_logprobs:\n        raise VLLMValidationError(...)\n\nif num_prompt_logprobs := self.prompt_logprobs:\n    if num_prompt_logprobs == -1:\n        num_prompt_logprobs = model_config.get_vocab_size()\n    if num_prompt_logprobs > max_logprobs:\n        raise VLLMValidationError(...)\n```\n\nThe lower V1 sampler then consumes the untrusted value:\n\n```python\nassert num_logprobs >= 0\ntopk_indices = torch.topk(logits, num_logprobs, dim=-1).indices\n```\n\nPrompt logprobs reach the same top-k helper through `compute_prompt_logprobs_with_chunking(...)`.\n\nvLLM intentionally exposes logprobs and prompt logprobs, but it does not intend one request to bypass the configured `max_logprobs` budget. The default `ModelConfig.max_logprobs` is 20, and the official launch docs explicitly warn that no-cap mode can return `output_length * vocab_size` logprobs and may cause OOM.\n\nThe Python API and engine-level `SamplingParams` validators implement that budget before engine execution. The Rust gRPC converter constructs engine-facing sampling state directly, so it must enforce the same safety contract before submitting a request.\n\nThis is distinct from:\n\n- `this report` / `GHSA-6pgw-f3pv-4h8q`, which covers the explicit `CandidateTokens.token_ids` selector lowering into `logprob_token_ids`.\n- `this report` / `GHSA-w2v2-5662-8v5q`, which covers out-of-vocabulary prompt token IDs.\n\nPublic PR `#44680` also notes that `top_logprobs`/`prompt_logprobs` are the same DoS class but out of scope for that token-ID parity patch because they need `max_logprobs` wiring.\n\n## Impact\n\nAn attacker who can reach a vLLM Rust gRPC endpoint can send a small gRPC request with `output_logprobs = true` and an oversized `output_candidates.top_n`, or with `prompt_logprobs = true` and an oversized `prompt_candidates.top_n`.\n\nExpected impact is single-request denial of service for affected deployments: the Rust gRPC frontend accepts the count, forwards it to engine-core sampling state, and the lower V1 logprob path can raise after execution begins. EngineCore treats uncaught worker exceptions as fatal engine errors.\n\nThis report does not claim code execution, data access, model compromise, persistence, or public internet exposure. It is an availability issue caused by missing Rust gRPC input validation and an unchecked unsigned-to-signed cast.\n\nSuggested severity: High if the Rust gRPC endpoint is network-reachable without an additional deployment-layer credential requirement.\n\nSuggested CVSS v3.1:\n\n```text\nCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\n```\n\nIf maintainers model the affected gRPC surface as reachable only to already-authorized clients, the conservative score is:\n\n```text\nCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H\n```\n\nSuggested CWEs:\n\n- `CWE-20`: Improper Input Validation\n- `CWE-400`: Uncontrolled Resource Consumption\n- `CWE-190`: Integer Overflow or Wraparound\n\n## Suggested Fix\n\nValidate `CandidateTokens.top_n` in `rust/src/server/src/grpc/convert.rs` before assigning `params.logprobs` or `params.prompt_logprobs`.\n\nRecommended patch shape:\n\n- replace `*n as i32` with a checked conversion;\n- reject values larger than `i32::MAX`;\n- reject values larger than the configured model/server `max_logprobs`, unless the operator explicitly configured no-cap `max_logprobs = -1`;\n- preserve the explicit `all` selector as the only way to request full-vocab logprobs;\n- return `Status::invalid_argument(...)` before submitting any engine request; and\n- add unary `Generate` and streaming `GenerateStream` regression tests for:\n- `top_n = 20` accepted under default config;\n- `top_n = 21` rejected under default config;\n- `top_n = 1_000_000` rejected;\n- `top_n = 2_147_483_648` rejected; and\n- both output and prompt candidate selectors covered.\n\n## Affected Package/Versions\n\n- Package: `vllm` (PyPI).\n- Affected versions: `>= 0.22.0, < 0.24.0`.\n- Patched version: `0.24.0`.\n\n## References\n\n- vLLM security policy: `https://github.com/vllm-project/vllm/security`\n- vLLM vulnerability management: `https://docs.vllm.ai/en/latest/contributing/vulnerability_management/`\n- vLLM `SamplingParams` API docs: `https://docs.vllm.ai/en/latest/api/vllm/sampling_params/`\n- vLLM `--max-logprobs` CLI docs: `https://docs.vllm.ai/en/latest/cli/launch/render/`\n- vLLM sampler docs: `https://docs.vllm.ai/en/latest/api/vllm/v1/sample/sampler/`\n- vLLM PR `#44680`: `https://github.com/vllm-project/vllm/pull/44680`\n\n## Appendix: Duplicate Review\n\nI checked local vLLM submission bundles, private-visible vLLM advisories, and GitHub PR/issue search for `top_logprobs`, `prompt_logprobs`, `max_logprobs`, `top_n`, `CandidateTokens`, and gRPC logprob variants.\n\nNearby but distinct:\n\n- `this report` / `GHSA-6pgw-f3pv-4h8q`: explicit candidate token IDs bypass `logprob_token_ids` bounds. This report covers count-based top-k selection and unsigned-to-signed overflow.\n- `this report` / `GHSA-w2v2-5662-8v5q`: prompt token-ID range validation.\n- `GHSA-3mwp-wvh9-7528`: unbounded OpenAI `n` fan-out DoS. This report uses a different gRPC response selector and a different lower sampler sink.\n\nPublic PR `#44680` names `top_logprobs`/`prompt_logprobs` as out-of-scope vectors that need `max_logprobs` wiring. PR `#45390` does not touch Rust gRPC files. Public GitHub PR and issue searches for the exact Rust gRPC `CandidateTokens.top_n` path returned no matches.\n","categories":["Unclassified"],"product":"vllm","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-rwfw-xvg3-3937","source_modified_at":"2026-10-09T07:47:04Z","fetched_at":"2026-10-26T06:00:26.954813Z","package":{"ecosystem":"pip","name":"vllm"},"range":">= 0.22.0, < 0.24.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-rwfw-xvg3-3937","source_modified_at":"2026-10-09T07:47:04Z","fetched_at":"2026-10-26T06:00:26.954813Z","package":{"ecosystem":"pip","name":"vllm"},"version":"0.24.0"}],"cvss":5.9,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-09T07:47:04Z","modified_at":"2026-10-09T07:47:04Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-rwfw-xvg3-3937","fetched_at":"2026-10-26T06:00:26.954813Z","source_modified_at":"2026-10-09T07:47:04Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-09T07:47:04Z","content_hash":"046ddc258e69e88dbcdeab24b7a93dcac80c120d867b4b69e03d162e75aee0eb"}}
