模型输出会被推理引擎当成可执行代码:vLLM 工具解析器如何跨越宿主机权限边界
部署过 vLLM 的工程师都熟悉这个画面:模型在 GPU 上吐出 token,流式地回到客户端。直觉上,推理服务只是一个矩阵乘法工厂,返回的是文本。但 2025 年 8 月,vLLM 官方安全公告记录了一次真实的服务器被攻破:一条被精心构造的模型输出,让推理服务在宿主机上执行了任意 Python 代码,CVSS 8.8,属于 High 级漏洞。
这篇专题要论证的不是「模型会不会恶意调用工具」这个老问题,而是更底层的一件事:推理引擎把模型可控的 token 从不可信数据升级成了可执行语义。模型生成的那串字节本身没有任何权限,但帮它解析的组件持有宿主机进程的全部权限,而且替它动了手。只要这条输入边界仍与高权限的 GPU 服务共进程,模型输出就可能借解析器夺取宿主机权限。
这怎么可能?要说清这次输出为何能越权,得先回到它离开 GPU 后被谁重新解释。
后面六章沿着一条线推进:先拆穿「推理服务只是矩阵乘法」的直觉,再看权限是怎么被借出去的,用 CVE-2025-9141 校准威胁的现实边界,用 LangSec 解释为什么失守是结构性的,最后落到架构取舍与一条今天就能落地的设计原则。
1. 推理服务不只是算矩阵乘法:模型输出回来后,还要被谁重新解释?
输出不是终点:token 还要被重新解释
当你向 vLLM 的 OpenAI 兼容接口发一个 chat 请求,服务端返回的既不是 GPU 直接吐出的原始 token,也不是简单透传。在模型输出和最终响应之间,隔着至少三层「解释」。第一层是聊天模板(chat template),它把模型输出包装成对话格式;第二层是工具调用协议,模型按约定输出带 XML 标签或 JSON 的文本,服务端把它解析成「要调用哪些函数、每个参数是什么」;第三层是类型转换器,把参数值从字符串转成 int、float、dict 这些真正的 Python 对象。
这三层做的事本质相同:把「文本」升级为「语义」。聊天模板决定文本怎么摆放;工具调用协议决定一段 <tool_call> 文本意味着「请执行函数 f、参数为 x」;类型转换器决定参数 x 是数字还是对象。也就是说,模型输出在抵达最终返回之前,已经被当作有执行意图的结构化数据处理过一遍,而这一步发生在服务进程里,不是发生在真空中。
V1 是多进程系统:输出字节要原路回到「输入处理」一侧
vLLM 的官方架构文档写得清楚:V1 采用显式的多进程架构。一个单机 4-GPU 的 TP=4 部署默认是 6 个进程,1 个 API Server、1 个 Engine Core、4 个 GPU Worker,进程间用 ZMQ 通信。API Server 处理 HTTP 请求和输入预处理(tokenization、多模态加载);Engine Core 跑调度器、管理 KV cache;GPU Worker 才真正加载权重、执行前向,是默认布局里唯一直接接触 CUDA 的进程。vLLM Arch Docs
反常的地方在这里:模型在 GPU Worker 上生成的输出字节,要经 Engine Core 通过 ZMQ 原路回到最左侧的 API Server,才被包装成 HTTP 响应发给客户端。也就是说,输出在返回之前跨越了进程边界,回到了「输入处理」那一侧。它不再天然是可信的内部数据,而是被一个同样处理外部请求的进程重新解释的东西。看一下官方这张进程拓扑图,这个空间关系一目了然。

图注:vLLM V1 的 TP=4 部署拓扑。左侧 1 个 API Server 通过 ZMQ 连接 1 个 Engine Core,Engine Core 再连右侧 4 个 GPU Worker(GPU 0..3),共 6 个进程。模型输出要从 GPU Worker 经 Engine Core 原路回到 API Server,才变成响应返回给客户端。vLLM Arch Docs
解析层与高权限宿主共进程,是官方自己承认的
那么工具解析器跑在哪个进程?按代码结构定位:vLLM 的工具解析器抽象基类位于 vllm/tool_parsers/abstract_tool_parser.py,而消费它的是 OpenAI 兼容的 chat-completion serving 层,也就是 API Server 进程的一部分。GPU Worker 只看到 token id,永远看不到原始的工具调用 JSON。这正是 CVE-2025-9141 埋雷的位置:解析发生在那个持有 HTTP socket、聊天模板、tokenizer、VLLM_* 环境变量和完整 Python 标准库(包括 eval(),一个能把字符串当场当作 Python 代码执行的函数)的进程里。顺带一提,修复后这个解析器文件在 main 分支上已更名为 qwen3_engine_tool_parser.py,但架构位置没有变。
vLLM 官方安全文档也以自承的方式确认了这套安排的暴露面:工具服务器默认不启用,必须显式 opt-in;Code Interpreter 的容器默认不做网络隔离,直接继承宿主 Docker 网络;Ray 集群被视为单一信任域,driver 的环境变量按 copy-all-except-denylist 策略全量传播给 worker。vLLM Sec Docs 换句话说,「解析层与高权限资源共进程」这一步由代码结构推断,「共网络、共信任域」则是项目文档白纸黑字写下的默认状态。
所以推理服务不是矩阵乘法工厂。返回给客户端的输出,在抵达之前已经被聊天模板、工具调用协议和类型转换器重新解释过,而这一步发生在一个持有 HTTP socket、集群凭据与完整 Python 标准库的进程里。那么问题来了:这个权力极大的解释器,要是被模型输出骗了呢?
2. token 本身没有任何权限:执行权是怎么被「借」出去的?
谁替 token 动了手
模型生成的 token 只是字节序列,没有身份、没有权限。真正有权限的是进程:API Server 进程以某个 OS 用户身份运行,持有网络 socket、环境变量里的凭据,以及 Python 的完整执行能力。当解析器把 token 当数据解析时,一切正常;但当它把 token 当代码、当对象、当控制指令时,它就是在用自己的进程权限,替没有权限的字节执行。这段关系就是本篇的核心概念「权限跃迁」:低权限的模型输出,借高权限解析器的身份获得执行权。
CVE-2025-9141 把这条借权路径完整走了一遍。漏洞出在 vLLM 的 Qwen3 Coder 工具解析器里。其中有个类型转换函数 convert_param_value(函数名以引入 commit 4594fc3b 的源码为准),负责把参数值从字符串转成正确的 Python 类型。对 string / int / float / boolean 它都有专门分支,但对没能识别出来的「未知类型」,它在 int、float、json.loads 三层都失败之后,落到了最后的兜底:eval(param_value),直接把参数字符串当 Python 表达式求值。
三前提与逐级升级链
完整触发路径需要三个前提同时满足:启动时启用自动工具选择(--enable-auto-tool-choice);指定使用 qwen3_coder 解析器(--tool-call-parser qwen3_coder);模型输出里的工具参数类型未被解析器显式识别。GHSA-79j6-g2m3 三条满足时,一条「模型兼容性 → 宽松回退 → eval()」的升级链就成立了:解析器为了兼容模型输出格式,一层层退让,最后把字符串升级为代码。下面的调用栈把这条链画全。
图注:CVE-2025-9141 的触发链路。模型输出的 XML 工具调用文本经聊天模板解析进入
_parse_xml_function_call,convert_param_value在类型无法识别时逐级回退,最终把参数字符串交给eval()在 API Server 进程内执行,完成权限跃迁。
修复长什么样:一行 eval() 的消失
修复它的 PR #23266「Do not use eval() to convert unknown types」的 diff 只有 13 行。修复前,类型转换失败会走 try: converted_value = eval(param_value);修复后,eval() 被直接删掉,换成打一条 warning 日志、把值当字符串原样返回。PR #23266 对比如下:
# 修复前:类型无法识别时的最后兜底(commit 4594fc3b 引入)
if param_type == "object" or param_type.startswith("dict"):
try:
converted_value = json.loads(param_value)
return converted_value
except json.JSONDecodeError:
logger.warning("... will try other methods to parse it.")
try:
converted_value = eval(param_value) # 漏洞落点
return converted_value
except Exception:
...
return param_value
# 修复后(PR #23266):删掉 eval(),未识别类型一律按字符串返回
logger.warning(
"Parameter '%s' has unknown type '%s'. "
"The value will be treated as a string.",
param_name, param_type)
return param_value
注意一个细节:最终合并进 main 的版本是把 eval() 删掉、退化为字符串,而不是换成 ast.literal_eval(只解析安全字面量的函数)。ast.literal_eval 只在同期 PR #23099 的 review 中间版本里被讨论过,最后被显式回退。这一刀的效果是:任意 prompt 让模型输出 <parameter>__import__('os').system('...')</parameter> 这种载荷时,旧代码会在 API Server 进程里执行它,新代码只会把它原样存进参数字符串。解析器替模型执行代码的通路,被切断了。
还有一条值得记住的工程证据:这个 eval() 在引入时(PR #21396)就被代码审查机器人标注为 critical,但合并者为了让新模型可用而强制合入。这不是没人发现的安全问题,而是被业务紧迫性显式压过去的失守路径。
所以,把字符串当代码执行的不是模型,是解析器。它用自己的进程权限,替没有权限的字节动了手。但这条路真的在现实里被走通过吗?证据链完整吗?我们看下一章。
3. CVE-2025-9141:真实失守过一次的边界,到底证明到什么程度?
硬事实:官方公告与四源交叉确认
2025 年 8 月 20 日,vLLM 发布安全公告 GHSA-79j6-g2m3-jgfw,对应 CVE-2025-9141:Qwen3-Coder 工具解析器中的不安全反序列化(CWE-502,大白话就是程序把不可信字节还原成可信对象的过程),允许任意已认证用户在满足前提时远程执行任意代码(RCE,Remote Code Execution),CVSS v3.1 8.8,属于 High 级。影响版本 ≥0.10.0 且 <0.10.1.1,修复版本 0.10.1.1。引入它的 PR #21396 在 2025 年 7 月 22 日合入,公告与修复在 8 月 20 日同日落地。
这些数字不是孤证。GHSA、OSV.dev、Tenable、Red Hat Bugzilla 四个来源对齐了版本边界:
| 来源 | 影响版本 | 修复版本 | 其他 |
|---|---|---|---|
| GitHub Security Advisory(GHSA-79j6-g2m3-jgfw) | ≥0.10.0, <0.10.1.1 | 0.10.1.1 | CWE-502,CVSS v3.1 8.8 |
| OSV.dev | 命中 0.10.0 与 0.10.1 | fixed 0.10.1.1 | severity HIGH |
| Tenable | 同一 CVE | 同一 CVE | CVSS v3 8.8 High,2025-08-21 收录 |
| Red Hat Bugzilla #2389395 | 同一 CVE | 同一 CVE | 以 CVE-2025-9141 别名跟踪 |
第三方复核:这是设计性回退,不是边界遗漏
Miggo Security 对引入 commit 4594fc3b 做了 commit 级复核,给出了一个比公告本身更重要的定性。convert_param_value 是 _parse_xml_function_call 方法内部嵌套的回退函数;「参数值不是标准类型、JSON 又解码失败时执行 eval()」这个行为,是类型转换的 fallback mechanism。也就是说,这是作者写下的「先 JSON、再回退到 Python 表达式」的设计,不是某行代码被意外漏掉。Miggo Miggo 还给出了一个工程上有用的指纹:运行时 profile 里的入口函数是 Qwen3CoderToolParser._parse_xml_function_call,流式与批处理共用同一个入口函数。
这个定性是本章的推力:不是补一处遗漏就能了事,问题出在回退设计本身。全局搜索同类模式会发现,其他解析器里同样分布着为兼容模型格式而设的宽松回退;同期合入的 PR #23099 只能逐个加固,堵不住「新解析器带来新回退」的节奏。
威胁的现实边界
也把话说回来。这个 CVE 证明边界真实存在且被突破过,但不证明任意模型都能自主发现利用链。攻击者需要让它部署的模型在自动工具选择模式下、用 qwen3_coder 解析器、输出一个类型无法识别的参数,这要求攻击者能操控进入模型的输入(prompt injection 或直接使用 API)。所以公告口径是「任意已认证用户」:认证边界不等于解析边界,这条漏洞的攻击面在解析层,不在鉴权层。
总结:这条边界真实失守过一次,而且是设计层面的回退。但利用有明确前提,威胁不等于「模型随时自我觉醒」。真正的问题转向了:为什么宽容的解析器会结构性失守?补丁能补到什么时候?
4. 宽容的解析器为什么会系统性扩大失守风险:一个 2011 年就写下的分析框架
输入即语言:LangSec 的三句话
这里需要一个安全理论框架:LangSec(语言论安全)。它的主张可以压缩成三句话。第一,程序收到的输入是一种「语言」。第二,处理输入的代码必须按这种语言的「识别器」来写,识别器必须强到足以识别该语言的全部合法输入。第三,如果一个系统的合法输入无法被简单解析,那它在实践上就无法被做成安全的。反过来说,ad-hoc(临时凑的)输入处理,校验散落在业务逻辑里、不断往后加回退,会制造一个「weird machine」。LangSec weird machine 是 LangSec 的术语,大白话就是:一个本该只被数据「喂」的程序,却被数据「指挥」了。解析器每加一条「为了兼容模型格式」的回退,就等于给数据多开放一条变成指令的通道。
这里要划一条边界:LangSec 是识别危险解析结构的分析框架,它并不单独证明每个宽容解析器都会形成可利用的 RCE。它的论点是,一旦系统落入某些解析结构,失守风险会被系统性扩大;至于具体能否被利用,还要看攻击面与前置条件是否齐备。
这套框架 2011 年就写在了 Dartmouth 的 LangSec 研究页和四篇代表论文里:2011 年 USENIX ;login: 的 weird machine 奠基、2012 年 IEEE S&P 对 Postel 宽容原则的修正、2012–2013 年的 shotgun parsers、2014 年 IEEE S&P 的 Beyond Planted Bugs。当年他们批评的对象是网络协议栈,如今同一套逻辑原样落在了 LLM 工具解析上:qwen3_coder 解析器「为了兼容模型格式逐级回退」的写法,正是 LangSec 识别出的、会系统性制造歧义输入的标准模式。下面这张图把失效链画出来。
图注:LangSec 的失效链。输入语言进入识别器,识别器本该拒绝一切非法输入;宽容的识别器放行了歧义数据,处理器把数据当作指令执行,程序变成可被数据编程的 weird machine。
生产里真有流量:工具调用与结构化输出
von Hippel 与 Miyazono 在 2025 年 IEEE S&P 的 LangSec 工作坊(SPW25)上把这套框架正式套到 LLM 上,并引述了一份带百分比的量化数据:按 Maitai 流量数据,约 32.7% 的请求涉及工具调用,约 21.2% 涉及结构化输出(structured output),两个类别的重叠极小。von Hippel 2025
图注:按 Maitai 客户流量数据(论文自标为 anecdotal 的单供应商样本),生产请求中约三分之一走工具调用解析路径,约五分之一走结构化输出解析路径。
更关键的是,von Hippel 指出主流 LLM 工具的解析「与主应用逻辑放在同一个地方,没有任何特别的 sandbox 或隔离」,这话与 CVE-2025-9141 的 eval() 路径在结构上等价。这解释了为什么 vLLM 的教训不是孤例。
同一个失守模式,在 agent framework 与工具协议层独立复发
同型失守并不只在 vLLM。LangChain 在五个月内连出两颗反序列化 CVE。GHSA-c67j-w6g6-q2cm(对应 CVE-2025-68664,2025 年 12 月,CVSS 9.3)是 dumps() 不转义 'lc' key,让用户数据在序列化往返中被当成 LangChain 对象;CVE-2026-44843(2026 年 5 月,CVSS 8.2)是运行时路径用 allowed_objects="all" 调用 load(),让任何 LangChain 可序列化对象被复活。LangChain GHSA 两颗 CVE 机制不同,但都完成了同一件事:把不可信字节升级为可信对象。
MCP(Model Context Protocol)层也一样。MCPXKIT(发表于 TDSC 2026)系统化了 31 种 MCP 攻击,核心发现之一是 agent 对工具描述存在「盲从」:工具 description 字段里的文字被模型当成指令级语义读取。MCPXKIT 工具描述就此成了解析器攻击面的更早入口,看这张 MCP 架构图。

图注:MCPXKIT 论文的 MCP 架构与威胁概览。左侧是 Host 与 Client,右侧 Server 内含 Tools、Resources、Prompts 三个模块,外围标注了读本地文件、查数据库、访问网络等威胁源位置(编号①–⑧)。工具描述正处在模型可读取并执行的上下文里,是比解析器更上游的攻击面。MCPXKIT
所以,宽容的 ad-hoc 解析器会系统性扩大失守风险,这不是 vLLM 的个别 bug,而是输入语言理论在 2011 年就识别出的结构性风险模式,如今在 LLM 工具解析、反序列化与 MCP 层同时重演。既然是结构性的,那补丁和 schema 到底能堵住什么?
5. 补丁和 schema 堵住已知路径,却管不住绕过后的爆炸半径:该隔离的究竟是哪一层?
17 类问题,塌缩成 4 个根因
Rashidi 2026 年 7 月的 execution-security 综述,把 2023 到 2026 年的 39 篇相关论文系统化为 17 类机制,然后做了一个关键操作:把这些类按根因重读,塌缩成 4 个反复出现的缺陷。Rashidi 2026 RC1:不可信内容与控制输入没有结构分离(transformer 把工具返回、网页文本、用户指令当成同类 token 处理);RC2:检查一次、永远信任(TOCTOU、MCP 工具投毒、单次批准即永远生效都同构);RC3:机制只回答「动作类型是否被允许」,不回答「这次调用是否符合当前意图」;RC4:防御都用作者自己造的 attacker 来评估,而不是真实部署工件。
这里要先分清两类手段的分工,这是个取舍,不是二选一。严格 schema 与补丁能阻断已知路径、缩小入口:schema 越严,攻击者送得进来的歧义数据越少,补丁则把已经暴露的洞堵上。但它们都不能替代隔离对绕过后的爆炸半径的控制。schema 约束的是模型输出的形状,约束不了解析器在形状之内怎么解释;补丁只修已知的洞,覆盖不了下一个解析器带来的新回退。爆炸半径只有隔离管得住。RC1–RC4 的塌缩指向同一件事:补丁式修复每条都只在某个根因的循环里打转,而且解析器随模型版本高速迭代,每条新回退都可能再造一个新入口。vLLM 修了 qwen3coder 的 eval(),下个 parser 呢?LangChain 硬化了序列化,五个月后又破了一次。只有把问题归到 RC1–RC4 这四个结构层面,才谈得上结构性解法。下面第一张图展示了 17 类如何塌缩到 4 个根因,第二张图则把每条故障链断在哪一步画了出来。

图注:Rashidi 综述中 17 类 execution-security 研究如何塌缩到 4 个根因:类别节点按其主要响应的根因连到 RC1–RC4,虚线边表示该类只是次级贡献。3 个类不直接对应任何根因,8 个类连到一个,6 个类连到两个,没有一类同时针对三个或四个。Rashidi 2026

图注:RC1–RC4 每条根因的故障链,以「前置条件 → 中间步骤 → 动作 → 结果」四步表示,红色断点标记每一步链上出错的位置。vLLM 的 eval() 回退、LangChain 的反序列化、MCP 的工具投毒,各自都能对齐到其中一条链上,说明失守是结构性的,不是个别 bug。Rashidi 2026
LASM 的层级定位:工具执行层是结构性失守点
Chu 在 2026 年 4 月的 LASM 综述里把 agent 栈切成 7 层,其中 L4 工具执行层(Tool Execution)的所有攻击,都被归到同一个结构性缺陷:principal trust inversion,字面意思是「主体信任倒置」。LASM 大白话是:工具返回的环境数据本来是体系里最不可信的来源(信任层级 Developer ≻ Operator ≻ User ≻ Environment),却未经任何标记就进入了规划层,被当成可信输入使用。对应的范式解法叫 trust-marking on tool returns:在表示层显式给工具返回打上「这是环境数据」的标记。这个原语无法被输入过滤或包签名替代,必须发生在数据表示层。

图注:LASM 的七层结构。L1 到 L5 运行时层(Foundation、Cognitive、Memory、Tool Execution、Multi-Agent)被 L6 生态边界包住(MCP server、软件包、模型 checkpoint),L7 治理面作为横切(cross-cutting)平面贯穿各层,点线是观察通道、虚线是策略写回。L4 工具执行层正位于运行时带内,是被生态边界包住、又被治理面穿透的一层。LASM
落到 vLLM 拓扑:该隔离的是哪一层
回到 vLLM 的 V1 拓扑,用 LASM 的 L4 镜头来读它。它已经做了进程级分离:API Server / Engine Core / GPU Worker 三个进程,ZMQ 在中间。但进程分离不等于权限分离。API Server 与 GPU Worker 虽已分进程,只要它们仍共享宿主机的权限域,比如以同一 OS 用户运行、凭据经 Ray driver 环境全量传播、网络没有网段隔离,解析器 RCE 的爆炸半径就照样够得着高价值资产。GPU Worker 被攻破直接交出模型权重;解析进程被攻破,拿到的是能触达集群的凭据与网络路径,顺着就能摸到权重和集群资源。这两头的终点,都在同一个爆炸半径里。
这条映射正是 LASM 点名的 principal trust inversion:解析器把自己的进程信任借给了不可信的模型输出,工具返回未经信任标记就进入执行路径。vLLM 里它比通用 agent 场景更直接:工具调用文本不是进「规划层」等某个 agent 决策,而是解析器在同一进程里直接把它变成 Python 对象和 eval() 调用。对解析结果做 trust-marking(信任标记)在这里的作用,是把「这一段是模型可控的不可信数据」显式留在数据表示层,让下游任何组件都无法把解析结果误当可信输入。但标记只解决信任区分,解决不了同一进程共享权限的问题,后者只能靠隔离。
工程答案分两步,先认清要降权隔离的是哪个进程、要保护的是什么资产。要降权隔离的,是解析进程:按 V1 默认架构,工具解析器运行在 API Server 进程内(由 vllm/entrypoints/openai/chat_completion/serving.py 的解析器导入链推断,官方文档并未直说),而 API Server 正是那个对外暴露、处理不可信输入、又持有凭据和完整 Python 标准库的进程。把解析留在那里,等于把「输入边界」和「高权限资产」放在同一个爆炸半径里。要保护的,是 GPU 与集群资产:模型权重、KV cache、Ray driver 与 worker 的集群凭据、能触达存储与其它节点的网络路径。这两类终点必须分处不同的信任边界,隔离才算成立。
具体手段可以按档位递进。vLLM 官方安全文档给出的硬化清单:denylist 式环境变量裁剪(写进 $VLLM_CONFIG_ROOT/ray_non_carry_over_env_vars.json)、用 secrets manager 或挂载文件注入凭据而不是放进 driver 环境、hidepid=2 或不同 OS 用户隔开 procfs、Ray TLS 与隔离网段限制集群访问。各档运维代价不同:denylist 属纯配置改动、代价最低,OS 用户或网络级隔离要改部署、代价更高。这是工程判断,vLLM 官方文档只按档位列出清单,并未比较成本高低,具体按自己集群的信任模型选。vLLM Sec Docs
多节点 DP 部署还要多留意一个点:DP Coordinator 是额外的信任集中点。看下面这张 17 进程拓扑,它通过 ZMQ 连接所有 API Server 与 Engine Core,但它只是 ZMQ peer,不是隔离边界。

图注:vLLM V1 在 TP=2、DP=4 下的 17 进程拓扑(4 个 API Server、4 个 Engine Core、8 个 GPU Worker、1 个 DP Coordinator)。顶部多出的 DP Coordinator 通过 ZMQ 连接全部 API Server 与 Engine Core,它是额外的信任集中点,但只是 ZMQ peer,不是隔离边界。vLLM Arch Docs
所以结论是:schema 与补丁缩小入口,隔离管理爆炸半径,两者是分工不是替代。不要赌解析器不犯错。把解析进程与持有权重、集群凭据的 GPU 宿主隔开,并最小化其网络与凭据权限,才能把单点 RCE 从「失去模型与集群」降为可遏制事件。
6. 把模型输出当不可信数据:一条今天就能落地的设计原则
防御性假设:解析器仍可能失守
把所有证据收拢,先立一条明确的防御性假设:工程上应按「解析器仍可能失守」来设计。这不是对某一次具体失守的预报,而是一种安全姿态:隔离与最小权限要按失守会发生来布局,而不是按失守不会发生来布局。上一章的 RC1–RC4 四条故障链说明,失守不是个别 bug,而是四种结构失败中的某一种;vLLM 的 eval()、LangChain 的反序列化、MCP 的工具投毒,各自都能对齐到其中一条链上。
支撑这条假设的是一组反复出现的风险模式,而不是对下一次失守的确定预测。按 Rashidi 综述的统计,截至其覆盖窗口(2026 年中),至少 4 个影响生产 agent harness 的同型 CVE 已修复:runc 的容器逃逸(CVE-2024-21626)、Copilot 的命令注入(CVE-2025-53773)、Claude Code 的 TOCTOU 与 API key 数据外泄(CVE-2025-59536、CVE-2026-21852)。Rashidi 2026 从 2025 年 8 月的 vLLM 到 2026 年的 Claude Code,失守路径一直在换皮重演。要分清这两件事:历史 CVE 证明的是「宽容解析器持续产出可利用缺口」这一模式在真实生态里反复出现,并不承诺下一次失守一定落在哪个解析器、哪个版本上。所以它支撑的是设计姿态,不是精确的威胁预报。
三条可执行原则
对部署开源模型、建设 agent 服务的工程师,今天就能落地三条原则。
第一,把模型输出当不可信数据。模型生成的 token 与网络请求一样不可信。凡是把解析结果交给高权限组件的地方,都按处理外部输入的标准来:解析器与主应用分开、跑在低权限账号下、拿最小化的网络与凭据权限。CVE-2025-9141 的三前提已经演示过:模型输出即攻击输入,不是修辞,是触发路径本身。
第二,把解析与采样同高权限 GPU 宿主隔离。V1 的多进程架构已经是现成骨架,把 tool parser 挪出 API Server 进程,或至少把 API Server 的凭据剥离,让一次解析器 RCE 连不上集群凭据。隔离是横跨 RC1–RC4 多重根因的结构性答案,补丁只在单个根因上打转。
第三,对任何「让解析器更宽容」的改动保持警觉。宽容度与攻击面成正比:为了兼容新模型而加的每条回退,都在扩大那条输入边界。反过来,严格 schema 与引导式生成(把输出约束在可识别语言内)是从源头缩小边界。
收口
回到开头的画面。推理引擎不是矩阵乘法工厂:它在返回模型输出之前,已经用聊天模板、工具调用协议和类型转换器把输出重新解释过,而解释器拿着宿主机进程的权限,替没有权限的 token 动了手。CVE-2025-9141 证明这条边界真实失守过,LangSec 证明失守是结构性的、补丁修不完。工程答案不是祈祷下一个 parser 写得完美,而是按不可信模型对待输出:隔离、最小权限、默认失守。你的推理服务值得这样设计,不是因为今天有人攻击你,而是因为解析器替你执行的那一行 eval(),在 2025 年真实存在过。
参考文献
本专题引用的核心资料如下,建议深入学习时阅读原文:
- Remote code execution in the vllm tool call parser for Qwen3-Coder (GHSA-79j6-g2m3-jgfw / CVE-2025-9141) — GitHub Security Advisory
- vLLM PR #23266: Do not use eval() to convert unknown types — GitHub Pull Request
- vLLM Architecture Overview (V1 Process Architecture) — docs.vllm.ai
- vLLM Security Documentation (Tool Server, MCP, Ray Trust Model) — docs.vllm.ai
- LangSec: Language-theoretic Security — Dartmouth Research Page
- AI Security is a LangSec Problem (von Hippel & Miyazono, SPW25) — PDF
- LangChain CVE-2026-44843 / GHSA-pjwx-r37v-7724 + GHSA-c67j-w6g6-q2cm — GitHub Security Advisory
- MCPXKIT: The Unified Toolkit for Analyzing Model Context Protocol Security — arXiv:2508.12538
- The Balkanization of Execution-Security Research for AI Coding Agents (Rashidi) — arXiv:2607.05743
- A Systematic Survey of Security Threats and Defenses in LLM-Based AI Agents: A Layered Attack Surface Framework (LASM) — arXiv:2604.23338
- CVE-2025-9141: vLLM Qwen3-Coder Parser RCE — Miggo Security Independent Technical Analysis — Miggo Vulnerability Database