最近看到anthropic公布了claude mythos发现的漏洞列表,于是尝试用DeepSeek“复现”是否也能分析出来。

Vulnerabity

访问anthropic Disclosure ledger,可以看到很多我们可以寻找已经修复了的漏洞来试着复现分析一下,看看DeepSeek是否能找出漏洞来。

CVE-2026-32316

我们挑选了CVE-2026-32316,对应ANT-2026-EBDTPNVH · jq,看描述是一个整数的溢出导致的堆缓冲区溢出。

页面描述“static analysis — not yet dynamically reproduced”,只是静态分析了,没有动态复现。

看CVE描述和advisories,我们可以看到jvp_string_append函数中,

// src/jv.c:1187
uint32_t allocsz = (currlen + len) * 2;
if (allocsz < 32) allocsz = 32;

两个变量相加之后,再乘以2,超出了uint32,整数上溢出了。

补丁,增加了判断。

 if ((uint64_t)currlen + len >= INT_MAX) {
    jv_free(string);
    return jv_invalid_with_msg(jv_string("String too long"));
  }

AI分析

我们找到1.8.1版本的jvp_string_append函数

static jv jvp_string_append(jv string, const char* data, uint32_t len) {
  jvp_string* s = jvp_string_ptr(string);
  uint32_t currlen = jvp_string_length(s);

  if (jvp_refcnt_unshared(string.u.ptr) &&
      jvp_string_remaining_space(s) >= len) {
    // the next string fits at the end of a
    memcpy(s->data + currlen, data, len);
    s->data[currlen + len] = 0;
    s->length_hashed = (currlen + len) << 1;
    return string;
  } else {
    // allocate a bigger buffer and copy
    uint32_t allocsz = (currlen + len) * 2;
    if (allocsz < 32) allocsz = 32;
    jvp_string* news = jvp_string_alloc(allocsz);
    news->length_hashed = (currlen + len) << 1;
    memcpy(news->data, s->data, currlen);
    memcpy(news->data + currlen, data, len);
    news->data[currlen + len] = 0;
    jvp_string_free(string);
    jv r = {JVP_FLAGS_STRING, 0, 0, 0, {&news->refcnt}};
    return r;
  }
}

我们先去问一下DeepSeek这段代码。 使用的语句是,

分析函数,static jv jvp_string_append(jv string, const char* data, uint32_t len) {
  jvp_string* s = jvp_string_ptr(string);
  uint32_t currlen = jvp_string_length(s);

  if (jvp_refcnt_unshared(string.u.ptr) &&
      jvp_string_remaining_space(s) >= len) {
    // the next string fits at the end of a
    memcpy(s->data + currlen, data, len);
    s->data[currlen + len] = 0;
    s->length_hashed = (currlen + len) << 1;
    return string;
  } else {
    // allocate a bigger buffer and copy
    uint32_t allocsz = (currlen + len) * 2;
    if (allocsz < 32) allocsz = 32;
    jvp_string* news = jvp_string_alloc(allocsz);
    news->length_hashed = (currlen + len) << 1;
    memcpy(news->data, s->data, currlen);
    memcpy(news->data + currlen, data, len);
    news->data[currlen + len] = 0;
    jvp_string_free(string);
    jv r = {JVP_FLAGS_STRING, 0, 0, 0, {&news->refcnt}};
    return r;
  }
}

我们的目的是先了解一下这个函数,然后再分析有没有漏洞,没想到分析之后,就发现了风险。

在思考过程中,我们可以看到

还有,在else分支中,新分配大小使用(currlen+len)*2,但可能导致分配过大?如果currlen+len很大,乘以2可能溢出32位?但currlen和len都是uint32_t,可能溢出,但实际字符串长度不可能超过2^31?jq可能限制长度小于2^31-1?但最好检查。这里没有溢出保护,有风险。

在回答正文部分,

5. 潜在问题与注意事项
5.1 整数溢出风险
(currlen + len) * 2 可能溢出 32 位无符号整数。例如 currlen 和 len 都接近 2^31 时,和接近 2^32,乘 2 会回绕到 0。这会导致分配过小甚至分配失败。

jq 实际使用中,字符串长度通常受 JSON 规范或内存限制,不会达到 2^31,但严格的代码应检查溢出。

5.2 频繁重新分配
对于大量小追加(如逐字符构建),指数增长策略会分配较多内存(最多浪费一倍),但摊还复杂度仍是 O(n)。原地追加路径可以优化连续追加的场景。

5.3 共享字符串的意外复制
如果字符串被多处引用,即使它有足够剩余空间,也无法原地追加,必须复制。这可能导致不必要的内存使用,但保证了语义安全。

5.4 对二进制数据的安全性
函数直接使用 memcpy,不会对 data 做任何假设,因此可以追加包含 '\0' 的二进制数据。字符串的长度严格由 length_hashed 管理,'\0' 仅作为辅助终止符。

可以看到,5.1部分就提示了整数溢出风险。

接着我们问一下安全风险。

有没有安全风险

思考过程

我们被问到“有没有安全风险”,需要分析这个函数是否存在安全风险。我们得从内存安全、整数溢出、缓冲区溢出、未定义行为等角度考虑。函数使用了memcpy、指针操作、长度计算等。潜在风险包括:
1.整数溢出:(currlen + len) * 2 可能溢出。currlen 和 len 都是 uint32_t,和最大约 4GB,乘2可能溢出到小值,导致allocsz过小,然后jvp_string_alloc分配不足,后续memcpy溢出。这是严重的安全风险。

下面的部分是整个输出:

是的,该函数存在几处潜在的安全风险,尤其是在输入长度较大或内存分配失败时。下面逐项分析。


1. 整数溢出风险(高危)

uint32_t allocsz = (currlen + len) * 2;
if (allocsz < 32) allocsz = 32;
  • currlenlen 均为 uint32_t,最大值为 2^32 - 1 ≈ 4.29 GB
  • currlen + len > 2^31 时,乘以 2 会导致 32 位无符号整数溢出,例如:
    • currlen + len = 0x80000000(2.147e9)
      → 乘以 2 → 0x00000000(0)
    • 随后 allocsz 被设为 32,远小于实际需要的内存。
  • 后续 jvp_string_alloc(32) 分配极小缓冲区,但 memcpy(news->data, s->data, currlen) 会复制远超 32 字节的数据,造成严重的堆缓冲区溢出,可导致任意代码执行或程序崩溃。

附加溢出点:

news->length_hashed = (currlen + len) << 1;
  • currlen + len ≥ 2^31,左移一位会丢失最高位,使 length_hashed 存储的长度值错误(例如原长度 3GB 会变成 (3GB - 2^31)*2)。后续使用该长度的操作将越界。

2. 分配失败未检查(中危)

jvp_string* news = jvp_string_alloc(allocsz);
// 未检查 news != NULL
memcpy(news->data, ...);  // 若 news == NULL,空指针解引用 → 崩溃
  • 如果 jvp_string_alloc 因为内存不足返回 NULL(或错误哨兵),后续 memcpy 会访问空指针,导致段错误。虽不一定被利用执行任意代码,但可造成拒绝服务(DoS)。

3. 剩余空间计算错误的风险(低危,取决于实现)

if (jvp_refcnt_unshared(string.u.ptr) &&
    jvp_string_remaining_space(s) >= len) {
    memcpy(s->data + currlen, data, len);
  • jvp_string_remaining_space 的实现有缺陷(例如未考虑对齐、未减掉终止符等),可能返回过大的值,使本应失败的检查通过,导致 memcpy 写入缓冲区之外。这是一个实现缺陷而非本函数直接错误,但调用者需保证该辅助函数的正确性。

4. 引用计数的竞态条件(多线程环境)

if (jvp_refcnt_unshared(string.u.ptr) && ...)
  • 函数假设在检查 unshared 之后没有任何其他线程修改引用计数。若无锁保护,其他线程可能同时克隆该字符串,使引用计数从 1 变为 2,导致本函数判断失误并原地修改实际被共享的字符串,破坏不可变性。这属于并发安全风险

5. 类型混淆风险

  • 函数直接使用 string.u.ptr 并强制转为 jvp_string*,未验证 string 是否真的是字符串类型。如果调用者传入错误类型的 jv(例如数组、数字),会导致任意内存读写。通常 jvp_string_ptr 内部会断言或检查,但此处未显示,风险取决于调用链。

6. 对非法指针或长度的依赖

  • datalen 由调用者提供,若 len 大于 data 缓冲区的实际大小(例如 data 未以空终止且 len 错误),则 memcpy 越界读取。这是调用者责任,本函数无法防御。

缓解措施建议

  • 整数溢出保护
    if (currlen > UINT32_MAX - len) abort(); // 或返回错误
    uint64_t need = (uint64_t)currlen + len;
    if (need > (UINT32_MAX >> 1)) abort();
    uint32_t allocsz = need * 2;
    
  • 分配失败处理
    if (news == NULL) { /* 清理并返回错误 jv */ }
    
  • 添加类型检查(调试模式可用断言)。
  • 多线程下加锁或使用原子引用计数操作。
  • 明确文档要求:调用者保证 data 至少可读 len 字节。

总结

风险类型 严重程度 利用可能性
整数溢出 → 堆溢出 较高(可控输入长度)
分配失败 → 空指针解引用 中等(内存压力)
并发竞态(引用计数) 取决于应用场景
剩余空间计算错误(依赖实现) 低-中
类型混淆/非法输入 低-中 低(调用者可控)

最需要修复的是整数溢出问题,它能够被精心构造的 JSON 或字符串操作触发,导致远程代码执行。建议在长度计算前进行饱和或溢出检查。

可以看到,第一条就分析出存在整数溢出风险的安全风险。

总结

可以看到,我们并没有使用复杂的提示语句,简单的一句话,就可以“复现”发现CVE-2026-32316。

不过,我们这里是简化了流程,直接给出了对应代码。如果是实际过程,如何在对大规模代码进行分析,是一个需要考虑的问题。还有一个就是误报率。