Skip to content

LATX: clear stale jump caches after invalid-TB SIGILL - #406

Open
rmjskhy wants to merge 1 commit into
lat-opensource:masterfrom
rmjskhy:temp1
Open

LATX: clear stale jump caches after invalid-TB SIGILL#406
rmjskhy wants to merge 1 commit into
lat-opensource:masterfrom
rmjskhy:temp1

Conversation

@rmjskhy

@rmjskhy rmjskhy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary / 变更说明

修复 LATX 在 JIT 代码失效期间可能反复执行无效翻译块、导致程序卡住的问题。

LATX 使用 0x88888888 非法指令标记已失效的翻译块。线程执行该指令产生 SIGILL 后,原处理逻辑可能没有彻底清除当前 CPU 中指向旧翻译块的缓存,导致调度器再次进入同一个旧块并反复触发 SIGILL

本次修改在处理该 SIGILL 时:

  • 使用原子比较交换清除仍指向旧翻译块的普通跳转缓存,避免覆盖并发写入的新翻译块。
  • 同时清除对应的 LATX 快速跳转缓存。
  • 返回调度器重新查找或生成翻译代码。

修改只作用于无效翻译块的异常处理,不在正常翻译块中增加检查指令。

Validation / 验证

在 LoongArch 主机上关闭 AOT 验证,设置 LATX_AOT=0

  • ninja -C build64 :编译通过。

  • 单独运行 TestMetaspaceAllocationFailure.java 中使用 CompressedClassSpaceSize=10M 的测试动作 10 次:

    • 10/10 均正常达到规定的 100 次循环。
    • 单次耗时 45 至 48 秒。
    • 未出现超时或 JVM 崩溃。
  • 连续运行以下完整测试 3 次:

    make test-prebuilt \
      TEST="jdk/jfr/event/runtime/TestMetaspaceAllocationFailure.java" \
      JTREG="VERBOSE=all"
    • 3/3 均得到 TEST RESULT: Passed. Skipped: jtreg.SkippedException: Exceeded MAX_ITERATIONS of 100
    • 耗时分别为 144、146、146 秒。
    • SkippedException 是测试达到 100 次循环上限后的预期结果。

Checklist / 检查项

  • I have read CONTRIBUTING.md. / 我已阅读 CONTRIBUTING.md
  • Every commit contains a DCO sign-off (git commit -s). /
    每个提交都包含 DCO 签署(git commit -s)。
  • I have included relevant build or test results, or explained why they
    are not applicable. /
    我已提供相关构建或测试结果,或说明了不适用的原因。

@LaurenIsACoder LaurenIsACoder left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我对这里的触发链还有疑问。现有 do_tb_phys_invalidate() 在普通 jump cache 仍指向失效 TB 时,已经会同时清理 fast jump cache:

if (qatomic_read(&cpu->tb_jmp_cache[h]) == tb) {
    latx_fast_jmp_cache_clear(cpu, h);
    qatomic_set(&cpu->tb_jmp_cache[h], NULL);
}

执行线程如果在清理前已经把旧 tc.ptr 加载到寄存器中,之后再进入一次 0x88888888 是可以理解的;但 SIGILL handler 会通过 context_switch_native_to_bt_ret_0 返回 dispatcher,而旧 TB 已经设置 CF_INVALID 并从 QHT 删除,正常情况下后续 lookup 应该重新查找或生成 TB,不应继续反复进入同一个旧块。

麻烦补充一下修复前 SIGILL 现场的实际 cache 状态,重点确认:

current_tb
current_tb->pc
current_tb->tc.ptr
hash
cpu->tb_jmp_cache[hash]
fast_jmp_cache[hash].pc
fast_jmp_cache[hash].ptr

特别想确认两点:

  1. 失效时是否确实出现了 cpu->tb_jmp_cache[hash] != current_tb,但 fast cache 仍然指向 current_tb
  2. 是否确认是同一个 current_tb 连续触发 SIGILL,以及第一次退出到 dispatcher 后为什么没有通过 CF_INVALID 检查和 QHT miss 恢复。

如果当时没有保留这些信息,可以加一版临时日志再跑原测试,至少把连续两次 SIGILL 的 current_tb、regular cache 和 fast cache 内容记录下来。这样才能判断是 invalidation 漏清 fast cache,还是已经加载的旧跳转、direct link/JRRA 等其他入口。

Concurrent JIT code invalidation can unlink a translation block while another thread is executing its two-word jump slot. Replacing the pair with one 64-bit store can combine an old pcaddu18i with a new jirl and send execution beyond the translated code.

Publish an in-range B only after placing a SIGILL sentinel in the second word, and restore the original pair while the old B still skips that word. Retry a transient jump-slot SIGILL from the first word. Do not directly chain targets outside the B range, where publishing a two-word far jump cannot be made safe with this slot layout.

When an invalid-TB sentinel reaches the signal handler, also remove matching regular and fast jump-cache entries before returning to the dispatcher. This prevents the dispatcher from repeatedly selecting the same invalid block and adds no checks or instructions to translated blocks.

Tested with 20 consecutive direct runs of the MaxMetaspaceSize action and four complete runs of jdk/jfr/event/runtime/TestMetaspaceAllocationFailure.java on LoongArch, with AOT disabled in source and option_fork_unlink enabled.

Signed-off-by: liuchaoyi <liuchaoyi@loongson.cn>
@rmjskhy

rmjskhy commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

我对这里的触发链还有疑问。现有 do_tb_phys_invalidate() 在普通 jump cache 仍指向失效 TB 时,已经会同时清理 fast jump cache:

if (qatomic_read(&cpu->tb_jmp_cache[h]) == tb) {
    latx_fast_jmp_cache_clear(cpu, h);
    qatomic_set(&cpu->tb_jmp_cache[h], NULL);
}

执行线程如果在清理前已经把旧 tc.ptr 加载到寄存器中,之后再进入一次 0x88888888 是可以理解的;但 SIGILL handler 会通过 context_switch_native_to_bt_ret_0 返回 dispatcher,而旧 TB 已经设置 CF_INVALID 并从 QHT 删除,正常情况下后续 lookup 应该重新查找或生成 TB,不应继续反复进入同一个旧块。

麻烦补充一下修复前 SIGILL 现场的实际 cache 状态,重点确认:

current_tb
current_tb->pc
current_tb->tc.ptr
hash
cpu->tb_jmp_cache[hash]
fast_jmp_cache[hash].pc
fast_jmp_cache[hash].ptr

特别想确认两点:

  1. 失效时是否确实出现了 cpu->tb_jmp_cache[hash] != current_tb,但 fast cache 仍然指向 current_tb
  2. 是否确认是同一个 current_tb 连续触发 SIGILL,以及第一次退出到 dispatcher 后为什么没有通过 CF_INVALID 检查和 QHT miss 恢复。

如果当时没有保留这些信息,可以加一版临时日志再跑原测试,至少把连续两次 SIGILL 的 current_tb、regular cache 和 fast cache 内容记录下来。这样才能判断是 invalidation 漏清 fast cache,还是已经加载的旧跳转、direct link/JRRA 等其他入口。

这个质疑是对的。之前的现场没有保存你列出的 regular jump cache 和 fast jump cache 内容,因此无法确认:

  1. cpu->tb_jmp_cache[hash] != current_tb 时 fast cache 是否仍指向 current_tb;
  2. 是否由同一个 current_tb 在退出 dispatcher 后连续触发块入口的 0x88888888。
    后续在修复版本上再次复现后,保留并检查了两个卡死现场。结果表明,原来把问题归因于“dispatcher 反复从 cache 取出同一个 invalid TB”并不完整,卡死还发生在另一条路径:
  • 执行线程在 TB 入口被标记失效前已经进入该 TB;
  • 另一个线程在 JVM 修改 JIT 代码时解除 TB direct link;
  • CONFIG_LATX_LARGE_CC 下的远跳转由 pcaddu18i + jirl 两条指令组成;
  • CONFIG_LATX_LAZYLINK 的 unlink 路径原来使用一次普通的 64-bit store 恢复这两条指令;
  • 执行线程可能看到旧的 pcaddu18i 和新的 jirl,从而计算出错误地址并越过 TB 末尾,把后面的 TBMini 数据当作指令执行。
    两次现场分别为:
  • PC 0xffda8b7180,TB 起始地址 0xffda8b7100,有效代码大小 0x40;
  • PC 0xffd851f180,TB 起始地址 0xffd851f140,有效代码大小 0x2c。
    两次 PC 都已经越过 TB 的有效代码范围,落入 TB 后面的管理数据,而不是停在 TB 入口的 invalid sentinel。
    因此重新整理了修复:
  • lazy unlink 不再直接写回整个 64-bit 跳转槽;
  • 近距离 B 跳转与原始两条指令之间按安全顺序更新;
  • 临时命中跳转槽第二条 sentinel 时,从第一条重新执行;
  • 超出单条 B 范围的目标不建立 direct link,因为当前两条指令的槽位布局无法安全地并发发布一个新的远跳转;
  • 只有 direct link 确实建立后,才执行依赖它的 flags 优化。
    SIGILL handler 中清理 regular/fast cache 的代码仍作为 invalid-TB sentinel 的防御处理保留,但我不再把它描述为这次卡死的主要原因。你要求的原始 cache 状态当时没有记录,因此也不能声称已经证明该缓存漏清场景。
    更新后的版本没有在正常 TB 中增加检查或指令。验证结果为:
  • MaxMetaspaceSize 子场景连续运行 20 次全部完成;
  • 完整 TestMetaspaceAllocationFailure.java 运行 20次全部得到 TEST SUCCESS;
  • 没有再次出现超时、SIGILL 循环或 JVM 崩溃。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants