从前端角度理解 CVE-2026-31431

从前端角度理解 CVE-2026-31431

如果你是写前端的,第一次看到 CVE-2026-31431 这种内核漏洞,大概率会觉得跟自己没关系------里面提到的 splice、Page Cache、加密模块、容器逃逸,没一个是日常会碰的。

但把这些术语剥掉,它讲的事情你其实很熟:

一份本来只能读的共享数据,被某个函数当成自己的私有数据原地改了。后面所有读这份数据的人,拿到的都已经是被改过的版本。

这种坑前端里随处可见。下面我就用前端的方式把它讲一遍。

先看一张总览图,整个漏洞的因果链就是这样:


起点:引用传递

JS 里改对象属性,不会复制对象:

js 复制代码
const config = { role: 'guest' };
const ref = config;
ref.role = 'admin';

config.role; // 'admin'

ref 看起来是个新变量,其实和 config 指的是同一块内存。你改 refconfig 跟着变。

小代码里这没什么,大型应用里它是 bug 之源:

  • 子组件直接改了 props
  • 工具函数偷偷改了你传进去的对象
  • 全局缓存被随手 push 了一个元素
  • 第三方库拿到对象后偷塞了点状态进去

这一类 bug 调起来都烦:你看自己的代码,"我明明没动它",实际上别人通过同一个引用动了。

CVE-2026-31431 是同一回事,场景换到了 Linux 内核,"对象"换成了 Page Cache。

Page Cache 是什么

Linux 读文件不会每次都跑去敲磁盘。第一次读完之后,文件内容会留一份在内存里,下次再读直接从内存拿。这块"留在内存里的文件内容"就是 Page Cache。

它和浏览器缓存的逻辑很像,只是住在内核里、给整台机器所有进程共用。你打开 /usr/bin/su,我打开 /usr/bin/su,容器里另一个进程打开 /usr/bin/su------读到的可能是同一份内存。

前端里的 内核里的
浏览器缓存 Page Cache
对象引用 内存页引用
深拷贝 复制一份内存页
改了共享对象 改了缓存页
全局状态被污染 Page Cache 被污染

如果这份缓存里装的只是普通文本文件,被改坏顶多是读出来的数据不对。但要是装的是 susudopasswd 这种和权限有关的程序------后面谁执行它,跑的就不是磁盘上那份了。


splice 想干什么:少拷贝一次

数据从文件流到 socket,按部就班的写法是这样:

text 复制代码
磁盘 → Page Cache → 用户态 buffer → 再写回内核 → socket

中间从内核往用户态搬一次,又从用户态搬回内核一次。数据量大的时候这两次拷贝很贵。

splice() 想省掉这趟来回。它让数据在内核里几个对象之间直接"挪位置",不经过用户态:

text 复制代码
Page Cache ──splice──▶ 管道 / socket / 加密模块 ...

但"挪"在工程上往往不是真挪,而是传引用------下游拿到的是 Page Cache 那块内存的引用,不是它的副本。

性能是上去了。代价是:下游必须老老实实把这块内存当只读数据。它一旦写一笔,写的就不是自己的副本,是全机器共用的那份缓存。

正常路径和漏洞路径的区别就在这一步:

flowchart LR subgraph 正常["✅ 正常 zero-copy"] A1[Page Cache] --> B1[splice 传引用] B1 --> C1[下游模块只读处理] C1 --> D1[缓存保持干净] end subgraph 漏洞["❌ 漏洞路径"] A2[Page Cache] --> B2[splice 传引用] B2 --> C2[下游模块原地写入] C2 --> D2[缓存被污染] end style D1 fill:#d4f4dd,stroke:#2d8a3e style D2 fill:#fde2e2,stroke:#c0392b

漏洞点:下游写了不该写的内存

CVE-2026-31431 的事故就在这里。

splice 把 Page Cache 的引用交给加密模块去处理。加密模块在某条路径上没把这块内存当只读,原地改了一笔。

写成 JS 大概是这种感觉:

js 复制代码
const cache = Object.freeze({ bin: 'su 的内容' });

function encrypt(buf) {
  buf.bin = '...';   // 它以为 buf 是自己的
  return buf;
}

encrypt(cache);

只不过 Object.freeze 在 JS 里会拦住你,内核里没这层保护------加密模块觉得自己拿到的就是普通缓冲区,写下去就真写下去了。

写完之后世界变成这样:

  • 磁盘上的 /usr/bin/su 一字节没动
  • 内存里的 Page Cache 已经是攻击者想要的内容
  • 后面任何进程执行 su,加载到内存里的是被改过的版本

为什么这种污染特别难发现

很多安全检测的思路是算文件哈希:

text 复制代码
/usr/bin/su 的 SHA256 还对得上吗?对得上 → 没事

这套逻辑在这次漏洞面前直接失灵:

  • 磁盘文件没动,哈希一直对
  • 但运行时谁也不会再去磁盘读,大家都从 Page Cache 拿
  • Page Cache 已经被改了

也就是"看起来正常,跑起来不对"。前端里偶尔也会遇到类似的事------源码没改,但运行时 store 被某个地方污染了,行为就是不对。区别是这里被污染的不是 Redux store,是操作系统底层。

用图来看更直观:

攻击链长什么样

具体怎么打不展开,只说轮廓:

text 复制代码
低权限进程
  → 准备一段特殊数据
  → 走 splice 进 zero-copy 路径
  → 引用指向某个敏感程序的 Page Cache
  → 触到加密模块那条会写内存的路径
  → 缓存里的程序内容被改
  → 谁再执行这个程序,行为就不是原来的了

翻译成前端语言:

text 复制代码
一个低权限的小脚本
  → 拿到全局 store 的引用
  → 调了一个有副作用的工具函数
  → store 被它改了
  → 后面所有组件读到的都是脏数据
  → 应用按攻击者的意图在跑

前端里这种 bug 顶多让页面崩或者数据错;内核里同样的 bug,能让一个普通用户跑出 root 权限。


顺带把容器一起带进来

容器有个常见的误解:以为它是虚拟机,里面外面完全隔离。

不是。容器和宿主机共用同一个内核,Page Cache 也归这个内核管。容器里读 /usr/bin/su,宿主机读 /usr/bin/su,背后很可能是同一份缓存。

这就意味着:容器里一个进程把 Page Cache 改坏了,宿主机和别的容器再去读,读到的就是被改过的内容。容器隔的是用户空间,没法隔内核------这也是为什么内核漏洞老和容器逃逸一起出现。

flowchart TD K[共享 Linux 内核] --> PC[共享 Page Cache] C1[容器 A
低权限进程] -->|触发漏洞| PC PC -->|缓存被污染| C2[容器 B] PC -->|缓存被污染| C3[容器 C] PC -->|缓存被污染| HOST[宿主机进程] style C1 fill:#fde2e2,stroke:#c0392b style PC fill:#fde2e2,stroke:#c0392b style C2 fill:#fff4cc,stroke:#b8860b style C3 fill:#fff4cc,stroke:#b8860b style HOST fill:#fff4cc,stroke:#b8860b

和前端 bug 同根

把术语去掉,这次漏洞踩的几个坑前端里全见过。

信任边界没守住。 前端是把用户输入当 HTML 渲染了;这里是把只读缓存当可写 buffer 用了。都是"这个数据应该当什么对待"没想清楚。

为了性能省了一步该做的事。 前端常见的是不深拷贝、复用对象、缓存计算结果,省下来的代价是状态被污染。splice 的 zero-copy 是同样的思路,省的是内核那次拷贝,代价是下游必须自觉只读。

表面没变,行为变了。 前端:源码没动,运行时 store 已经脏了。内核:磁盘文件没动,Page Cache 已经脏了。两边都让人查半天查不出来。

flowchart LR subgraph 前端 F1[全局 store] --> F2[第三方工具函数] F2 -->|原地修改| F3[store 被污染] F3 --> F4[组件读到脏数据
页面错乱] end subgraph 内核 K1[Page Cache] --> K2[加密模块] K2 -->|原地写入| K3[缓存被污染] K3 --> K4[程序读到改过的内容
权限被突破] end F2 -.同一种 bug.-> K2 F3 -.同一种后果.-> K3 style F3 fill:#fde2e2,stroke:#c0392b style K3 fill:#fde2e2,stroke:#c0392b style F4 fill:#fff4cc,stroke:#b8860b style K4 fill:#fde2e2,stroke:#c0392b

修起来的思路

修这种 bug 没什么花活,就一句:

下游可能写这块内存,就别给它共享引用,先复制一份给它。

JS 写出来是这样:

js 复制代码
// 不行:直接改共享对象
function process(cfg) {
  cfg.temp = true;
  return cfg;
}

// 行:先复制一份
function processSafely(cfg) {
  const copy = structuredClone(cfg);
  copy.temp = true;
  return copy;
}

内核修起来麻烦得多,要判断哪条路径下游会写、哪条不会、哪些场景能继续 zero-copy、哪些必须降级到普通拷贝。但思路就是这个:这块内存归谁、能不能写,得说清楚。

flowchart TD subgraph 修复前 A1[Page Cache 共享页] --> B1[splice 直接传引用] B1 --> C1[加密模块在共享页上写入] C1 --> D1[全机器缓存被污染] end subgraph 修复后 A2[Page Cache 共享页] --> B2{下游是否会写?} B2 -- 会写 --> E2[复制到私有缓冲区] E2 --> F2[在副本上写入] F2 --> G2[共享缓存保持干净] B2 -- 不写 --> H2[继续 zero-copy] H2 --> G2 end style D1 fill:#fde2e2,stroke:#c0392b style G2 fill:#d4f4dd,stroke:#2d8a3e
相关推荐
AGoodrMe2 小时前
swift基础之async/await
前端·ios
irving同学462382 小时前
从零搭建生产级 RAG:Embedding、Chunking、Hybrid Search 与 Reranker
前端·后端
卡卡军2 小时前
vue3-sketch-ruler v3 升级详解:从 Vue 组件到跨框架标尺引擎
前端
还有多久拿退休金2 小时前
让看不见的 AI 动手画画——我意外造出了一个"绘图 Agent"
前端
陆枫Larry2 小时前
一次 iOS 橡皮筋弹性滚动的排查:从 absolute 到 fixed
前端
灏仟亿前端技术团队2 小时前
拆解亿级 SaaS 平台:Shopify 前端技术生态与架构避坑指南
前端
亲亲小宝宝鸭2 小时前
如何监听DOM尺寸的变化?element-resize-detector 和 resizeObserver
前端·javascript
胡志辉2 小时前
本地 AI 编码助手从 0 配起来:先选模型,再接 Ollama、VS Code、Claude Code 和 Codex
前端·后端
一颗小青松2 小时前
uniapp输入框fixed定位,导致页面顶起解决方案
前端·uni-app