Repository navigation
Conversation
x86 allows any alignment, and the i386 ABI aligns uint64_t struct fields to 4 bytes, but the aarch64 gadget sent every address that isn't 8-byte aligned to segfault_write without setting tlb->segfault_addr. The guest got a SIGSEGV or, if the stale address was mapped, retried forever. Python 3.14 (Alpine 3.24) hits this at startup. Do the unaligned case in C, through the TLB under a lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This is not correct; you can't just put a lock around cmpxchg to fix its semantics. Doing this properly is quite difficult and the segfault was intentionally added in lieu of that. #1429 has some discussion from a past attempt at trying to implement this. |
|
Fair enough, and thanks for the pointer to #1429. This was sent as the smallest change that gets Python 3.14 running; a fully correct unaligned atomic is a much bigger piece of work, and it would help to agree on a direction before anyone spends that time. One thing worth knowing either way: the segfault path doesn't fire reliably. It branches to |
Sure
I don't think this has the correct semantics? It's not not an atomic CAS |
A misaligned locked read-modify-write (i386 lock cmpxchg8b, and every unaligned locked op on amd64) took the global atomic_l_lock around a read and a write. That serialised locked ops against each other and nothing else: a plain store from another thread could land in between and be lost. That is the objection raised upstream (ish-app#2813, ish-app#1429). The i386 ABI makes the 64-bit case ordinary, since it aligns uint64_t struct fields to 4 bytes, and Python 3.14 does one at startup. Now exact, in emu/tlb.c: - Inside one 16-byte-aligned block: a 16-byte host compare-exchange on the block, changing only the operand's bytes. It is a real atomic against every access, other processes included. - Straddling two blocks (and maybe two pages): the address space's lock, taken as a writer. No guest code or guest-memory syscall runs in the process while it is held. The upgrade never drops the read side (read_to_write_lock_timed): this runs inside the JIT holding jetsam_lock, and dropping it let mprotect in to wait for jetsam while this waited for mprotect -- a deadlock, reproduced and sampled. One thread upgrades at a time, siblings are poked out of the JIT, and on contention or timeout the instruction restarts via a resolvable fault. Processes sharing the page are not held off in this case. The i386 cmpxchg8b helper now uses x86_atomic_cas. Test: tests/manual/x86_unaligned_lock runs one thread doing locked increments against one doing plain stores, on operands at block offsets 4, 12 and 1, across a page, and (amd64) 32- and 16-bit ones. Before this change it lost up to 60% of the stores on amd64 and 17% on i386, while the locked counts were exact. After it, zero lost; it passes on alpine-i386, alpine-amd64 and devuan-amd64 and skips on arm64. The 52 futex, lock, mmap, fork, swap and JIT tests pass on i386, amd64 and arm64. Python 3.14's unaligned atomics sit at block offset 4 (fast path) in steady state; a few at offset 12 occur at startup. The xcodebuild iphoneos build succeeds. Not covered: the i386 JIT's 16- and 32-bit locked gadgets still run an exclusive on a misaligned address and fault (queued in docs/TODO.md); the test leaves those cases out on i386. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, I'll send the SIGBUS fix. On the block approach, I think it is an atomic CAS. For what it's worth, my fork does exactly this now. A test running locked increments against plain stores on the same 4-byte-aligned |
|
The SIGBUS fix is up as #2816. |
On aarch64,
lock cmpxchg8bon an address that isn't 8-byte aligned goes straight tosegfault_write. x86 allows any alignment, and the i386 ABI alignsuint64_tstruct fields to 4 bytes, so 64-bit atomics on struct fields land here. Python 3.14, which Alpine 3.24 ships, does one during startup, sopython3can't run at all.It shows up as a SIGSEGV or a hang. The branch never sets
tlb->segfault_addr, so the fault reports whatever address is left there. If that address is mapped, the page-fault handler returns and the instruction retries forever.This does the unaligned case in C (
helper_cmpxchg8b_unaligned), reading and writing through the TLB under a lock. A page-straddling operand works, and a real fault reports its own address. The alignedldaxr/stlxrpath is unchanged, and so is the x86_64 host, whose ownlock cmpxchg8bhandles alignment.Tested with the new
tests/e2e/cmpxchg8b, in which four threads increment a 4-byte-aligneduint64_tmid-page and across a page boundary. It segfaults before this change and counts correctly after. On the Alpine 3.24 x86 rootfs,python3now starts and runs stdlib imports, threads, HTTPS and venv.🤖 Generated with Claude Code