Spill The Beans 精读:在共享 CPU 上从 embedding 表里偷出 LLM 的输出 token

和受害者租在同一颗物理 CPU 上的另一个进程,只要盯住 embedding 表里自己挑出来的那几百个 token 的缓存行、用 flush+reload 一圈圈轮询,就能在 LLM 生成的时候把落在这些位置上的 token 逐个记下来——在多租户部署里,这条路子能单次捞走 80%~90% 的高熵 API key。

论文:Spill The Beans: Exploiting CPU Cache Side-Channels to Leak Tokens from Large Language Models(arXiv:2505.00817v1,2025-05-01;MITRE 公开批准号 25-0107)

作者:Andrew Adiletta(MITRE)、Berk Sunar(Worcester Polytechnic Institute,伍斯特理工学院)

本地部署 LLM 的出发点是把数据留在自己机器上,但推理过程仍要走过共享硬件:同一颗物理 CPU 上另一个没有特权的进程,可以从缓存里把这次推理用到的 token 读出来。这条思路已经有好几种做法(抓 PCIe 明文流量、盯缓存访问模式、捡显存残渣),Spill The Beans 是其中最朴素的一种——不做时序信号处理,不训语言模型,也不还原输入,只用一轮 flush+reload 回答一个工程问题:在缓存行被驱逐之前,我最多能盯得住多少个 token。

📚 基本概念速读

名称 定义 记忆点
Embedding layer(嵌入层) 一张「token id → 稠密向量」的查表结构,每一行对应词表里的一个 token 全文唯一的观测面:查了哪一行,就暴露了哪个 token
GGUF llama.cpp 系的模型文件格式,分为 Header、Metadata & Tensor Info、Embedding Layer、Other Tensors 四段 偏移量从公开格式里就能自己算,不需要模型或框架的任何秘密
Flush+Reload 共享内存下的缓存侧信道原语:擦掉一条缓存行,等一会儿再读一次并计时 读得快 = 这期间有人把它搬回来过
clflush x86 指令,把某个地址所在的缓存行从各级缓存里踢出去 给每一轮扫描创造干净的初始状态
RDTSC x86 读时间戳计数器的指令 计时精度的来源,攻击者用它给每次 reload 打时间
Round-robin 监控 对一组候选 token 的地址循环扫,每圈都 flush 一遍再 reload 一遍 「能盯几个 token」这个问题的载体
Overhead(检测开销) 从检测到一次命中、到能继续下一轮扫描之间花掉的计算时间 开销越大漏得越多:每多 100 µs,可泄漏的 token 比例掉约 5%
Cache eviction(缓存驱逐) 一条缓存行被后来的访存挤出缓存 大词表的天然对手,也是全文权衡的来源
超线程(Hyper-Threading / SMT) 一个物理核当两个处理器用:多一份寄存器状态,执行部件与 L1/L2 是共享的 判断两个进程共享到哪一级缓存
Sibling cores(兄弟核) 两个进程放在各自有私有 L1 的两个核上,只共享 L2/L3 论文里攻击进程的默认落点
Unified memory(统一内存) CUDA 6/8 起提供的机制,CPU 与 GPU 共享同一套地址空间,靠页错误维持两边数据一致 论文「CPU 上能观察到 GPU 访问」这一解释的支点
KSM / 页去重 内核把内容相同的物理页合并成一份并标成写时复制(COW) 攻击者拿到共享内存的另一条路子
高熵凭据(UUID API key) 形如 b81132e3-2129-42e3-91f4-d8b64c93fbec 的 36 字符随机串 被 tokenizer 切成一批低频 token,候选集反而更小、更好盯
Zipf 定律 自然语言里第 n 个常见词的频率大致正比于 1/n 盯住最常见的一千个词,就能覆盖英文文本的一大块

🎬 场景:不是「本机」,而是「同租一台机器」

先看这篇论文把攻击者放在哪儿。

IKnowWhatYouSaid 的场景是本地部署——攻击者以普通应用的身份和 LLM 跑在同一台用户机器上,还能读到同一个模型文件。这篇换成了多租户:受害者把 LLM 跑在共享服务器上,攻击者只是同一台物理机上租了另一个实例的另一个用户。论文列了三种具体场景:消费级应用背后的 LLM 服务、企业内部共享服务器上同事之间的窥探、以及把监控能力塞进浏览器标签页。

威胁模型写得比前几篇更克制,四条边界都在收紧:

  1. 目标系统没有任何软硬件漏洞,进程隔离、虚拟机隔离、沙箱全都正常在位;
  2. 攻击者只是与受害者共处同一颗物理 CPU,只有软件层面的处理器与内存访问权,不需要物理距离,不需要示波器或探针这类物理手段;
  3. 攻击者拿不到受害者的 GPU,但 CPU 内存空间的物理页是共享的(物理上共享、逻辑上仍然隔离);
  4. 受害者的推理本身跑在 GPU 上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌───────────────────────┐
│ Victim Process │ ← 受害者:LLM 推理跑在 GPU 上
│ 1. LLM inference │
│ 2. embedding lookup │ ← 查表这一步在 CPU 上发生
└───────────┬───────────┘
│ process isolation still holds ← 两者在同一颗物理 CPU 上,隔离守住了内存,没守住缓存
┌───────────┴───────────┐
│ Spy Process │ ← 攻击者:只测时间,不与受害者通信
│ 1. flush + reload │ ← 轮询自己算出的候选地址
│ 2. measure latency │
└───────────┬───────────┘
↓
┌───────────────────────┐
│ Shared LLC │ ← 攻击的观测面:embedding 表的缓存行
│ + physical memory │
│ cache lines of W │
└───────────────────────┘

第 3 条和第 4 条放在一起,就是这篇论文最需要解释、也最容易被忽略的一处:推理在 GPU 上跑,embedding 表按理应该待在显存里,那么只守着 CPU 侧的攻击者凭什么看得到?

🔍 泄漏源:embedding 是查表,不是乘法

LLM 把输入输出都表示成 token 序列,模型要先给每个 token 找一条稠密向量,这一步叫 token embedding。写成矩阵形式是对 one-hot 编码做一次线性投影 E = W·x;因为 one-hot 里只有一个 1,实现上不会真去做矩阵乘法,而是退化成取 W 的第 i 行——查表。

查表就是访存,访存要经过缓存,于是「第 i 行被载入缓存」这件事可以被外部观察到:

1
2
3
4
5
6
7
8
                ┌──────────────────────────┐
(index i) ─────→│ Embedding Table W │ ← V × D 的参数矩阵(FP16)
│ row 0 │
│ row 1 │
│ row i │ ← 命中:处理 token i 时只读这一行
│ ... │
│ row |V|-1 │
└──────────────────────────┘

攻击者要算的,只是「第 i 行落在哪个地址」。 GGUF 的 Metadata & Tensor Info 段里直接记着 embedding 层的字节偏移、embedding 层的总大小、词表里 token 的总数,于是两条公式就够:

1
2
3
Size_token    = Size_embedding_layer / Number_of_tokens      (2)

Offset_token = Offset_embedding_layer + Token ID × Size_token (1)

这两步不需要模型权重、不需要框架源码、不需要知道模型结构——文件格式是公开的,算偏移是纯算术。论文因此把「校准」这一步列成攻击流程的第二环:先算地址,再谈监控。

🔗 为什么「GPU 上的访问」CPU 也能看到

这是这篇论文与前几篇最不一样的地方,也是它给人留下疑问最多的地方。

论文的解释是 NVIDIA CUDA 8(2017)引入的统一内存:GPU 代码可以超量分配内存(像 CPU 上的虚拟内存那样),而且 GPU 侧的页错误机制让 CPU 与 GPU 之间不需要显式同步就能保持数据一致。既然 embedding 向量留在统一内存里,两台设备上就各有一份同步的副本;攻击者在本地 CPU 上跑 flush+reload,就能监视到 embedding 表的访问。

需要留意措辞:原文用的是 “we suspect that the attack capitalizes on unified CPU/GPU memory introduced in Nvidia Cuda 8”——这是作者的推测,不是论文验证过的机制。论文后续的实验都在一台带 NVIDIA 显卡的机器上做(overhead 实验用 2070S + CUDA 后端,英文文本实验用 llama.cpp 跑 8B 模型),但全文没有安排一个把 embedding 表钉在显存上、再从 CPU 侧观察命中的对照实验。这一点在文末的口径一节还会再提一次。

🛠️ 校准:命中 100 cycle,未命中 370 cycle

在动手偷 token 之前,论文先做了一件很基础的事:量清楚命中和未命中的延迟差多少。

实验设计刻意最小化:跑两个进程,一个反复读 GGUF 文件里某个地址的字节,另一个在那个地址附近做 flush+reload。这里没有引入推理,也没有真正跑模型——作者自己写明了,就是为了先量出系统噪声(比如预取器活动)的影响,并顺便对比两种核亲和性配置。计时用 RDTSC,擦除用 clflush。

结果很干净,两个分布几乎不重叠:

1
2
命中(cache hit):约 100 cycles
未命中(miss) :约 370 cycles

阈值的取法是「覆盖 99% 的命中延迟,再加一个标准差」,这样阈值稳稳落在两个分布中间,不会来回误判。这个校准是后面一切的基准——判定「这一行刚才被访问过」的能力,最终就落在这两个数字的差距上。

🎧 攻击进程放哪个核:兄弟核还是同一颗核

接下来论文问了一个工程问题:攻击进程该跑在哪里?它试了两种配置。

🧵 先说清楚「超线程」是什么

两种配置的差别最终都落在缓存上。

超线程(Hyper-Threading,Intel 对同步多线程 SMT 的实现)是把一个物理核当成两个处理器交给操作系统用。 物理核内部的执行部件只有一套(算术逻辑单元、浮点单元这些),但核里多留了一份架构状态(寄存器、程序计数器),于是操作系统看到两个逻辑处理器,可以各自跑一条线程,两条线程的指令在同一个核里交错发射、共用那套执行部件。

它和双核不是一回事:双核是两套完整的执行部件、各自有私有的 L1;超线程共享执行部件,L1 与 L2 也是共享的,只有寄存器状态各自一份——这也是它的性能提升远不到一倍(通常只有一两成)的原因。

放到侧信道上有一条容易搞反:同一个物理核上的两个超线程,L1 是同一份。所以两个进程能互相看到多细,看的不是同不同核,而是它们共享到哪一级缓存。

  • 兄弟核(sibling cores):两个进程放在各自有私有 L1 的两个核上,只共享 L2 与 L3。
  • 同一颗核(same core):两个进程放在同一个核上,L1、L2、L3 全部共享。

两种配置的表现差别很明显:

配置 观测到的命中分布 论文的解释
兄弟核 命中主要落在被访问地址的附近,而不是地址本身 共享的 L2/L3 贡献了附近地址的命中,可能还受推测执行与预取机制影响
同一颗核 命中精确落在被访问的地址上,周围也有 共享 L1 带来的、更细粒度的访问模式,检测精度更高

论文据此下的结论是:两个进程共享 L1 时(同一颗核的配置),检测内存访问更精确。 这句结论本身没问题,但和后面的实验安排有点对不上——round-robin 监控(§4.4)和英文文本泄漏(§6)里,攻击进程都被明说 pinned to a sibling core。论文没有解释这个选择,本篇精读把它记在文末的口径表里。

🎯 从单个 token 到 round-robin

有了地址和阈值,剩下一件事:怎么盯。

论文先用最朴素的形态做了证明:给 Meta 的 Llama 模型一个只含单个 token 的 prompt,然后在预期的那条 embedding 向量地址附近做 flush+reload。对 5 个不同的 token 各跑一遍,得到的命中分布各不相同——也就是说,从缓存命中的位置上,确实能分辨出模型此刻在处理哪个 token。论文把这一步称为(在它知道的范围内)第一个从 LLM 里泄漏 token 的 CPU 缓存侧信道。

注意这里说的是观测到的命中分布不同,还不是「输出文本被还原」。要往文本上靠,就得同时盯多个 token,于是有了 round-robin:

1
2
3
4
5
6
7
攻击进程 pin 在一个核上,对候选 token 的地址循环执行:

1. Access 读一次这个 offset 上的缓存行
2. Time 用 RDTSC 记这次读的耗时
3. Flush 立刻 clflush 掉同一条缓存行
4. Record 若耗时低于阈值(如 200 cycles),记一次命中 → token ID
5. Next 轮到下一个候选 token

阈值 200 cycles 落在 100 与 370 之间。还有一条实现约束:检测期间不写文件、不做磁盘 I/O,所有后处理都推到监控循环结束之后——因为一旦在循环中间插入重活,后面几个 token 就可能在这段时间里被漏掉。

⚖️ 全文的核心权衡:盯多少个 token

到这里,论文最有价值的一节来了。

候选 token 盯得越少,轮一圈越快,回到某个地址时它的缓存行大概率还在,命中就抓得住;但能覆盖的词表范围也越小。盯得越多,覆盖面越大,可轮一圈的时间线性变长,回到这个地址时缓存行早被驱逐了,反而漏得多。

两边都不划算,中间存在一个甜点。先把两头摆清楚:

监控集大小 轮询一圈的代价 后果
只盯 5 个 token 极快,几乎不会漏 覆盖太窄,就算一个不漏,也看不出受害者在说什么
盯 1000 个以上 一圈要走很久 回到某个 offset 时,它的数据大概率已被驱逐,检测不到

论文先把这个代价量化了。它在一个 1 亿参数的小模型上(CUDA 后端,NVIDIA 2070S)监控 200 个 token 做 round-robin,然后人为插入不同长度的「检测开销」,看 200 个 token 里能抓到多少:

1
结论:检测开销每增加 100 µs,可泄漏到的 token 比例下降约 5%。

这个数字直接推着作者把检测路径做得尽可能轻。至于甜点在哪,论文做了两组独立的实验,答案落得很接近:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
可泄漏到的 token 比例
^
| ___________
| / \___
| / \___
| / \___
| /
| /
+--+---------+----------------------+----> 监控的 token 数
50 150 1000
└ 太少 └ 拐点 150~250 └ 太多:轮询一圈太久,回来时已被驱逐

§4.4(用已知输出的 prompt 做的对照实验):甜点约 200 个 token
§6 (英文文本):150~250 是拐点,超过之后边际收益递减

「约 200 个 token」就是这篇论文的核心工程结论。 论文也承认,这个数字随模型的 embedding 尺寸、缓存特性、token 生成的时间规律而变,需要现场标定——但它点出了要害:既不是越少越好,也不是越多越好,中间那一段才两头都占。

🔑 案例一:从 prompt 里的 API key 捞走凭据

甜点确定之后,论文拿它去打两种目标,先说最值钱的那种。

场景是很多人的真实习惯:把 API key 直接贴进 prompt,让 LLM 帮你写调用的代码。

1
2
3
4
5
6
7
8
9
User: Please write a script that checks the value of my Bitcoin wallet.
My API key is: "b81132e3-2129-42e3-91f4-d8b64c93fbec".
Please only give me the code

Llama:
import requests
def get_bitcoin_balance():
api_key = "b81132e3-2129-42e3-91f4-d8b64c93fbec"
response = requests.get(f"https://pro-api.coingecko.com/api/v3/...")

🎲 反直觉的地方:高熵反而更好抓

按常识,随机的高熵字符串应该比自然语言难猜。这里正好相反,原因在 tokenizer 上。

LLM 的分词器主要在自然语言上训练,所以常见词有专门的 token;而 b81132e3 这串十六进制字符不符合自然语言分布,会被切成若干个平时很少出现的子串 token。于是攻击者要盯的候选集合反而变小了——不用去覆盖「def、and、import 这些 Python 代码里的高频词」那一大片,只要盯住对应这些罕见子串的少数几个地址。

换句话说,UUID 的「随机」保护的是暴力猜解,在这里却变成了帮凶:它把需要监控的地址压缩成了一个稀疏的集合。

📈 需要盯多少、要聊几轮

论文先做了一次统计:生成 10 万个随机 UUID 并按目标模型分词,再用另一组 10 万个 UUID 去评估——在被监控的 token 数量与「整条 key 被完整捕获」的概率之间,存在一条明确的曲线。

监控的 token 数 整条 API key 被完整捕获的概率 平均覆盖到的 key 内容
250(策略起点) 1.4% 86.3%
1250 83.4% —

这两个数字看着矛盾,其实是同一枚硬币的两面:86.3% 是「平均能看到多少内容」,1.4% 是「一次不漏地看到全部」。平均每七个 token 里就有一个没被盯住,而一条 key 要完整拼出来,这些缺口得一次都不落下——所以覆盖率可以很高,全捕获概率仍然很低。论文的对策,就是把「重复出现」当成资源用起来:

  1. 先广后窄地轮换:第一轮宽范围监控(例如最靠前的 250 个 token),记下哪些位置命中了;没命中的那些,说明它们属于这个凭据里更难碰上的子串;
  2. 下一轮换一批:把已经确认过的 token 划掉,换成另一片区域里、上次没监控的 token;
  3. 用户和模型的对话里,key 常常会出现不止一次(贴一次、让模型写进代码一次、再让模型重构一次),每出现一次就是一轮新的机会。两三次曝光之后,完整捕获的概率就能被推到接近 100%。

论文在 llama.cpp(Intel Comet Lake 服务器 CPU + NVIDIA 2070S)上端到端跑了一组实验,结果是把「监控多少 token」换算成了「要聊几轮」:

监控集大小 泄漏 50% 需要几轮 泄漏 80% 需要几轮 泄漏 100% 需要几轮
50 1 7 66
100 1 4 33
200 1 1 18
250 1 1 15
300 1 1 12
350 1 2 12
400 1 2 9

这张表把权衡讲得最直白:盯 50 个 token 时,一轮就能捞出一半内容,但要捞全得聊 66 轮;盯 400 个时,9 轮就够。 论文由此得出对 API key 的总体结论——一次就完美还原不太现实,但平均能泄漏 80%~90%,再配合重复曝光和动态挑选,整条 key 大概率能拼回来。

🗣️ 案例二:普通英文文本能捞多少

第二种目标是纯英文对话,没有高熵凭据可捡,只剩下最普通的话。

监控集是这样挑的:从 Cornell Movie Dialogs 语料里抽出原始台词做词频分析,取频率最高的一千个 token——主要是冠词、连词、介词(and、the、it、to 这一档)。依据是 Zipf 定律:第 n 个常见词的频率大致是 1/n,所以一千个高频词就能覆盖英文书面材料的很大一块。这一步有个坑:LLM 的分词器经常把词切成子词,所以高频「词」还得逐个对齐到模型 GGUF 里真正的 token ID,监控集才算准。

攻击设置则更贴近真实:从 Quora 问题集里随机取问题(例如 What is the best way to learn Python?)作为单次 prompt,模型是 Meta-Llama-3.1-8B-Instruct-Q8_0.gguf,走 llama.cpp;攻击进程 pin 在兄弟核上,对那一千个目标 token 做 round-robin。攻击者不假设任何关于问题内容或回答内容的先验知识。

然后变化监控的 token 数(50 到 650),每个配置都只跑一次(single-shot),看能抓到回答里多大比例的 token:

1
2
3
4
5
6
7
8
9
10
11
可泄漏的英文 token 比例
^
| ______
| / \____
| / \___
| /
| /
| /
+--+-------+------------------------+----> 监控的 token 数
50 150 650
└ 太少 └ 拐点 150~250 └ 开销与驱逐开始占上风

结果和上一节是同一个形状:盯 150~250 个 token 时收益最高,再往上加只增加了往返延迟和驱逐概率,白花力气。 论文给出的数字是单次可达约 40% 的恢复率(写在摘要与 §1 的贡献列表里),并且强调这个比例高度依赖所选的监控集——针对更专门的输出领域(比如固定格式的代码、日志),覆盖率还能再提。

一个值得注意的推论是:「攻击者只对随机数据感兴趣」这个假设站不住。 即使是普通的英文提问,只要被推出来的比例够高,也可能暴露个人细节、医疗信息或企业机密。

🚧 论文自己指出的改进方向

论文在收尾时列了三条可以往下走的路,其中两条已经有别的论文在做:

  • 换用别的缓存侧信道:Prime+Probe 和 Prime+Scope 都不需要共享页,在大规模监控和信号稳定性上可能更好;代价是需要更懂缓存的索引与关联度参数,而且换个 CPU 型号就得重新摸一遍。
  • 上下文感知的轮换监控:让监控集跟着已经捞到的内容变。最简单的例子是——检测到句号这类句末 token,就把监控重点切到句首更容易出现的词(比如首字母大写的词);更复杂的做法是用马尔可夫链或概率语言模型预测下几个最可能出现的 token,把不相关的监控项换掉。
  • 用语言模型做后处理:漏掉或者拼歪的 token,用已知上下文让 LLM「填空」。论文特意指出这一步是离线做的,不会干扰实时测量——同源的 IKnowWhatYouSaid 就是把这条做成了攻击管线的主干(用两个微调模型分别做去噪与排序),而这篇只把它列为「未来可以加」。

🛡️ 对策与代价

论文给的对策覆盖应用层、系统层与硬件层,共同点是都要在性能或部署上付账:

措施 做法 代价
时间与空间随机化 推理时随机去读 embedding 层里那些用不到的段,把「缓存命中」和「真的在用这个 token」之间的关联冲淡;或者学密码库的常数时间写法,让向量访问的模式变得整齐 额外的访存会让推理变慢;常数时间访问是否实用,论文说还需进一步研究
不让攻击者共享内存页 保证攻击者与受害者不共享含模型参数的物理页,在 hypervisor 或容器层面强制隔离,不共享库和模型 隔离本身有性能开销;这一条能挡住依赖共享页的 flush+reload
硬件层缓存划分 用 Intel CAT 之类的技术按进程/虚拟机切分缓存路数,或者用 CacheGuard 这类带分区能力的缓存设计 需要特定硬件支持,公开云与消费级平台不一定提供;论文把它算作最直接有效的缓解
关掉页去重(KSM) 关掉内核同页合并,攻击者就拿不到去重产生的共享页 内存占用上升。论文明确说这条一般不推荐,理由就是这份开销(原文 performance losses)不划算

📎 读原文时要注意的几处口径

精读对照时,有几处值得先分清楚,免得引用时张冠李戴:

位置 需要注意的地方
机制是推测 全文「CPU 上能观察到 GPU 内存访问」这一环,建立在对 CUDA 统一内存一致性的猜测上(原文 we suspect),论文没有设计对照实验来验证 embedding 表真的驻留在显存一侧、又能从 CPU 侧被观察到
同一问题上有相反判断 IKnowWhatYouSaid(USENIX Security 2025)对同一问题做了对照实验,结论是「一旦 embedding 查表被放到 GPU 上,CPU 缓存侧信道就看不到了(离散 GPU 的缓存与 CPU 不保持一致)」,并且列出了唯一失败的框架配置。两项工作在这里的分歧,正好说明缺的那一步验证有多关键
两个百分比不是一回事 摘要里的「80%~90%」指的是平均泄漏到多少比例的 key 内容;Figure 8 里的「1250 个 token 对应 83.4%」指的是整条 key 被完整捕获的概率。前者是覆盖率,后者是「一次全对」的概率,混用会得出很不一样的结论
「首个」是自称 论文多次声明自己是第一个用硬件级缓存侧信道泄漏 LLM token 的工作(to our knowledge)。它与同期另一项工作(IKnowWhatYouSaid)同源——都是 flush+reload 盯 embedding 表——但一个只做输出、靠一轮轮询;另一个做了输入输出双向还原和语言模型驱动的去噪管线
核亲和性前后不一致 §4.2 的结论是「两个进程跑在同一颗核上检测更精确」,但 §4.4 与 §6 的实际实验都把攻击进程 pin 在兄弟核上,论文没有解释这个选择
地址那一步没有交代 公式算出的是 GGUF 文件内的字节偏移,而 flush+reload 需要的是攻击者进程地址空间里的虚拟地址。论文没有写明攻击者是怎么把文件偏移变成自己可 flush 的地址(同源的 IKnowWhatYouSaid 里,攻击者用 mmap 把模型文件映射进自己的地址空间来共享物理页,这一步是写明的)
40% 没有落到图上 英文文本「单次可恢复约 40%」这个数字只出现在摘要与 §1 的贡献列表里,对应的 Figure 9(§6.1)只给出曲线形状,没有标出各监控配置下的具体百分比

✅ 总结

在同一颗物理 CPU 上共享缓存的另一个进程,可以盯着 embedding 表里自己选定的那几百个 token,用 flush+reload 一圈圈轮询,把模型生成时落在那几个位置上的 token 逐个记下来。论文真正解决的问题不是「能不能看见」,而是「在缓存行被驱逐之前,最多能盯住多少个」——答案是两百个左右。

三个可以带走的要点:

  1. 这是缓存侧信道里最朴素的一种形态:没有时序信号处理,不训模型,只用一轮 flush+reload 加一张阈值表。它换来的能力也相应有限——只还原输出、只覆盖被监控的那部分词表,单次约 40% 的英文 token、80%~90% 的高熵凭据。
  2. 全文的核心是一个两端都堵的权衡:盯得少则轮询快、抓得住,但看得窄;盯得多则看得宽,但一圈走完缓存行早没了。约 200 个 token 是两头相交的位置,也是这篇论文最实用的一条结论。
  3. 「高熵反而更好抓」值得单独记住:Tokenizers 是为自然语言训练的,UUID 这种随机串被切成一批罕见子串 token,候选集因此变稀疏;再叠上「凭据会在对话里重复出现」这一点,一次抓不全的概率可以在两三轮里补回来。

这篇论文的位置可以用一句话概括:它把「共享缓存里的访存模式会泄漏 token」最早摆到了台面上,但也是最朴素的一次尝试——不做还原、不处理噪声,只回答监控范围该取多大。它的威胁模型最贴合云上多租户——攻击者不需要读模型文件,不需要物理接近,只要和别人租到同一颗 CPU 上。至于「GPU 上跑的推理为什么能从 CPU 看见」这个环节,它是靠推论串起来的,而这一点恰恰是后来那项工作做了对照实验、并给出相反结论的地方——读这一篇时,最该带着疑问的就是这里。

🔗 这条路线上还有哪些工作

工作 来源 偷什么 入手点
Hermes Attack USENIX Security 2021 模型本身(结构、超参数、权重) CPU 与 GPU 之间的 PCIe 明文流量
IKnowWhatYouSaid USENIX Security 2025 输入与输出文本 CPU 缓存的访存模式 + 时序
LeftoverLocals Trail of Bits 技术报告 GPU 显存里的数据残渣 同一块 GPU 上未初始化的 local memory
Spill The Beans arXiv 预印本 2505.00817v1 输出 token CPU 缓存里 embedding 表的缓存行

(本文为 arXiv:2505.00817v1 论文 Spill The Beans: Exploiting CPU Cache Side-Channels to Leak Tokens from Large Language Models 的精读笔记,正文表格内的数据取自论文原文。)