Skip to content

jit-icache-coherence: clean the data cache before invalidating the instruction cache on aarch64 - #14454

Open
doracawl wants to merge 2 commits into
bytecodealliance:mainfrom
goliajp:aarch64-icache-clean-to-pou
Open

doracawl wants to merge 2 commits into
bytecodealliance:mainfrom
goliajp:aarch64-icache-clean-to-pou

Conversation

@doracawl

@doracawl doracawl commented Sep 30, 2026 •

Copy link
Copy Markdown

clear_cache on aarch64 only issued ic ivau with a fixed 64-byte stride and never cleaned the data cache to the point of unification, so on cores with CTR_EL0.IDC == 0 the invalidated instruction cache refetched the old bytes, the new ones still sitting in a dirty data cache line above the point of unification. Fresh pages hide this because the Linux kernel cleans a page the first time it becomes executable; rewriting code in a page that has already executed gets no such help, and that is exactly what a JIT reusing code memory does.

This switches to compiler-rt's __clear_cache sequence (https://github.com/llvm/llvm-project/blob/3390613ccf0fbbb40026dcbabb36fce444f79480/compiler-rt/lib/builtins/clear_cache.c#L121-L153; the Linux kernel's caches_clean_inval_pou_macro, https://github.com/torvalds/linux/blob/551c722f40809618230001baccf219193e22fc5a/arch/arm64/mm/cache.S#L28-L43, is the same apart from using dsb ishst when IDC is set): dc cvau per DminLine unless CTR_EL0.IDC, dsb ish, ic ivau per IminLine followed by dsb ish unless CTR_EL0.DIC, then isb, with CTR_EL0 read once. compiler-rt uses this inline sequence on every aarch64 target except Apple and Windows. mrs ctr_el0 at EL0 gets SIGILL on macOS, so the Apple path calls sys_icache_invalidate as compiler-rt does on Darwin (https://github.com/llvm/llvm-project/blob/3390613ccf0fbbb40026dcbabb36fce444f79480/compiler-rt/lib/builtins/clear_cache.c#L225-L227); the Windows path already uses FlushInstructionCache and is unchanged.

On Graviton1 (Cortex-A72, IDC=0) the reproducer from #14442 over 200,000 iterations ran the previous function 146,886 times with 49.0.1's clear_cache and 0 times with this branch's final commit (a1.xlarge); the count varies between runs, 180,425 on an a1.medium with the previous revision, as in the commit message. The new hardware test fails at the first rewrite on the old code there, but it can only fail on IDC=0 cores, so the CTR_EL0 decoding is unit-tested separately; the crate's tests also pass on macOS (M4 Max), an RK3588 (A55+A76, IDC=1) and under qemu-aarch64 (cortex-a53, max).

Fixes #14442

…struction cache on aarch64

`clear_cache` on aarch64 only ran `ic ivau` over the range with a fixed
64-byte stride. On cores with CTR_EL0.IDC == 0 the dirty data cache line
was never cleaned to the point of unification, so the invalidated
instruction cache refetched the old bytes. Rewriting a function in a page
that had already executed ran the previous function 180,425 times out of
200,000 on a Cortex-A72 (AWS a1.medium); the kernel only cleans a page the
first time it becomes executable, which is why fresh pages hide this.

Use the sequence compiler-rt's `__clear_cache` and the Linux kernel's
`caches_clean_inval_pou_macro` use: `dc cvau` per DminLine unless IDC, `dsb ish`,
`ic ivau` per IminLine unless DIC, `dsb ish`, `isb`, with the line sizes
read from CTR_EL0 once. Reading CTR_EL0 at EL0 gets SIGILL on macOS, so
that path calls `sys_icache_invalidate` like compiler-rt does on Darwin.

Add a test that rewrites code in an executed page and checks the new code
runs, and a unit test for the CTR_EL0 decoding.

Fixes bytecodealliance#14442

@cfallin cfallin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks -- some questions below, but in general I am happy to see a more correct coherence implementation on non-Darwin aarch64!

if ctr == 0 {
unsafe {
asm!("ic ivau, {}", in(reg) start);
asm!("mrs {}, ctr_el0", out(reg) ctr, options(nomem, nostack, preserves_flags));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this cause any problems on big.LITTLE systems where different core types may exist? Or are they architecturally guaranteed to have the same cache coherence behavior? (Can you cite the relevant bit of the Arm ARM if so?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(By "does this cause problems" I mean specifically the caching, which is process-wide and survives across threads and across a single thread migrating between cores)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Partly architectural. The Arm ARM's CTR_EL0 description (DDI 0487 M.d, D24.2.41) requires the two bits that pick the maintenance to agree: for DIC, "All PEs in the same Inner Shareable shareability domain must have a common value of this field", and for IDC, "... must have a common Effective value of IDC". The line sizes have no such rule. For those it's the OS: Linux (since 4.9, 116c81f427ff), NetBSD 10 and FreeBSD 15 trap EL0 reads of CTR_EL0 on mismatched systems and return one system-wide value with the smallest line sizes. Where the OS doesn't, re-reading on every call wouldn't help either, since the thread can migrate between the mrs and the loops; compiler-rt's __clear_cache caches it the same way.

On an RK3588 (4x A55 + 4x A76), both clusters report the same IDC, DIC and line sizes; they differ only in L1Ip.

I put the reasoning and the Arm ARM wording in a comment above the static in f29d643.

}

#[cfg(target_arch = "aarch64")]
#[cfg(all(target_arch = "aarch64", target_vendor = "apple"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason we can't use the existing implementation on aarch64-apple-darwin, since it comes from Darwin source (so is known to be correct on that platform) and avoids a call into system libraries?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes: the Darwin source that sequence follows is the 2017 libplatform. The sys_icache_invalidate that ships today (libplatform-306 and later, macOS 14+) also issues an extra dsb ish after every 20 ic ivau on the CPU families in its cpus_that_need_dsb_for_ic_ivau table (cache.s#L31-L92); I checked the disassembly of libsystem_platform.dylib on macOS 27 and it matches. An inline copy would miss that and any later change. The 2017 version also has a dsb ish before the loop that the inline code doesn't (D7.5.9.15 makes ic ivau unordered against earlier stores without it).

Calling it adds no dependency, since std already links libSystem. Added this to the comment in f29d643.

…ll on Apple

Say in the comments why reading CTR_EL0 once per process is safe: IDC and
DIC must be the same on every core of an Inner Shareable domain (Arm ARM,
CTR_EL0), and Linux, NetBSD 10 and FreeBSD 15 hand EL0 a uniform value
for the line sizes. On Apple, keep calling `sys_icache_invalidate`: since
macOS 14 it issues an extra `dsb ish` every 20 `ic ivau` on some CPU
families, which an inline copy of the sequence would not.

Move the per-line iteration into `cache_lines` with a unit test, and add
CTR_EL0 decoding cases for the Cortex-A55, A64FX and what Linux reports on
a Neoverse-N1 with erratum 1542419.

prtest:macos-arm64 prtest:linux-arm64

@cfallin cfallin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK, the substance of this looks good now -- thanks for the persistence in finding sources!

The last thing I want to see cleaned up a bit is the "cfg soup" -- there are a lot of complex cfg conditions in this one file, and each top-level decl needs one; it'd be better if we split out per-system-config implementation modules and had one top-level cfg to use the module and re-export (pub use impl::*; kind of pattern). See e.g. here for a good example.

Once you do that refactor, I'm happy to merge; thanks!

This branch has not been deployed

No deployments
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.

jit-icache-coherence: aarch64 clear_cache skips dc cvau and assumes 64-byte cache lines

2 participants