TL;DR warning: some thinking out loud, don't read unless you're interested in the Linux kernel fork() mechanism and the Arm architecture.
fork() in Linux duplicates the parent page tables into the child process while marking the PTEs read-only. In the parent process, there's a single TLBI ASIDE1IS at the end of the page table copy (and before the child is started). There is no need for a TLBI in the child process since it starts with its own ASID and presumably no stale TLB entries for the new ASID (when the ASIDs run out, there's a full local TLBI on each CPU - TLBI VMALLE1; we call this a roll-over event).
The stack smashing check failure looks like copy-on-write (CoW) does not always happen for the stack page when both the parent and the child process access it shortly after fork(). The stack is likely the first page accessed after the fork() and, when the bug triggers, either the parent or the child succeed in writing it without triggering a permission fault into the kernel (for CoW). This typically happens if there are stale TLB entries.
I think we have two main scenarios after fork():
FWIW, Linux/KVM had a bug in this area, fixed about 8 years ago - https://lore.kernel.org/all/[email protected]/.
fork() in Linux duplicates the parent page tables into the child process while marking the PTEs read-only. In the parent process, there's a single TLBI ASIDE1IS at the end of the page table copy (and before the child is started). There is no need for a TLBI in the child process since it starts with its own ASID and presumably no stale TLB entries for the new ASID (when the ASIDs run out, there's a full local TLBI on each CPU - TLBI VMALLE1; we call this a roll-over event).
The stack smashing check failure looks like copy-on-write (CoW) does not always happen for the stack page when both the parent and the child process access it shortly after fork(). The stack is likely the first page accessed after the fork() and, when the bug triggers, either the parent or the child succeed in writing it without triggering a permission fault into the kernel (for CoW). This typically happens if there are stale TLB entries.
I think we have two main scenarios after fork():
- The parent writes the stack without CoW. Since we had a TLBI ASIDE1IS already, that's very unlikely, especially if the parent is not migrated to another vCPU (which may run on another CPU). Well, there's a small chance that the parent migrated to another vCPU (and on a different physical CPU) and the TLBI ASIDE1IS did not get propagated there for some hardware reason. I find this unlikely
- The child writes the stack without CoW. This would not be possible if the TLB cache is empty for the new ASID. However, we can have an ASID roll-over given that Apple Silicon only exposes 256 ASIDs, at least to the VM (a shell script with lots of forking would quickly run through them). Sub-scenario (a) is that M4 does some TLB sharing between CPUs but the local TLBI (non-inner-shareable) that Linux does on ASID roll-over doesn't invalidate all such shared TLBs, things can go wrong with stale TLB entries. A more likely possibility is (b) the hypervisor framework does not properly invalidate the TLB when multiplexing multiple vCPUs on the same CPU.
FWIW, Linux/KVM had a bug in this area, fixed about 8 years ago - https://lore.kernel.org/all/[email protected]/.