Fix SeqLock cacheline layout and CACHELINE_BYTES on aarch64 (tyndall #18) - #1
Conversation
…#18) The port of BluEye-Robotics/tyndall#18 ("Fix seq_lock cacheline padding and CACHELINE_BYTES on aarch64", issue #16), so a Rust record and a C++ record are the same bytes again once tyndall is bumped past c7fadae: - CACHELINE_BYTES is 64 on aarch64 (Jetson Orin, Cortex-A78AE; verified on target in the tyndall issue), as on x86_64; armv7 (i.MX) keeps 32. - `entry` is forced onto its own cacheline through a cacheline-aligned wrapper (tyndall's `alignas(CACHELINE_BYTES) STORAGE entry`), so it sits at offset CACHELINE_BYTES on every ABI regardless of the storage type's alignment. The old `[u8; CACHELINE - 4 - size_of::<usize>()]` padding assumed `seq` and `size` pack back to back; on 64-bit targets they do not, and `entry` landed at 36/40 (aarch64) or 68/72 (x86_64). - The layout is pinned at compile time (`const` assertions over 1-, 4- and 8-byte-aligned storage, and a post-monomorphisation guard against an over-aligned storage type, tyndall's static_assert), plus the runtime layout test of tyndall's tests/ipc/seq_lock_layout.cpp in Rust. Breaking on the wire for aarch64 and x86_64 (armv7 is unchanged), hence 0.3.0: a 0.2.x writer and a post-#18 C++ reader disagree, and vice versa, so this ships together with the tyndall bump on the drone image. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…riter Two review findings on the layout port: - The storage-alignment guard (tyndall's static_assert) was referenced only from write and read, so a program that merely opened a record over an over-aligned storage type built fine and created a segment no C++ instantiation can have. The segment size now comes from SeqLock::segment_size, which evaluates the guard, so opening trips it too; the module doc no longer claims `cargo check` catches it (a generic guard fires at build time, only the concrete layout assertions at check time). - write() now stores sizeof(STORAGE) in the header's `size` field, as tyndall's writer has since Feb 2025; its ipc_read tool reads it, no reader depends on it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Existing shared-memory segments need an explicit migration or rejection mechanism to prevent incompatible reads after upgrades.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Ports Tyndall #18’s corrected SeqLock ABI to Rust.
Changes:
- Corrects cacheline sizes and payload alignment.
- Adds compile-time layout guards and runtime tests.
- Bumps
ipc_libto 0.3.0.
File summaries
| File | Description |
|---|---|
ipc_lib/src/seqlock.rs |
Implements aligned ABI layout and tests. |
ipc_lib/src/lib.rs |
Uses guarded segment sizing. |
ipc_lib/Cargo.toml |
Bumps package version. |
Cargo.lock |
Updates locked package version. |
Review details
- Files reviewed: 3/4 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // Through `segment_size`, not `size_of`: that is where the storage | ||
| // type's alignment guard is evaluated (see seqlock.rs). | ||
| let size = SeqLock::<T>::segment_size() as off_t; |
There was a problem hiding this comment.
Not a concern here: we do not upgrade a live system. The tyndall change and this one ship together in an image, /dev/shm is tmpfs and is empty after the boot into that image, so no segment ever outlives the binaries that created it.
For the record, since I measured the layouts while checking this:
| Target | Old entry offset / size | New entry offset / size |
|---|---|---|
| armv7 (the i.MX drones) | 32 / 64 | 32 / 64 |
| aarch64 (Jetson) | 36 / 64 | 64 / 128 |
| x86_64 (dev, CI) | 68 / 128 | 64 / 128 |
Three things follow. The i.MX fleet is byte-identical before and after, so nothing changes there at all. On x86_64 the segment size does not change, only the payload offset, so a size-based migration check would never fire on the platform where the silent-mismatch risk is highest. And mapping the new size over a stale smaller object does not fault: /dev/shm is tmpfs, which allocates whole pages, so reads and writes past the recorded size still work and are visible across processes. I tested that. The only way to get wrong data is an old writer meeting a new reader, which is a mixed rollout.
The suggested remedy is also not available to us. The segment name and the header layout are the wire contract with tyndall's C++ side: IPC_SHMEM_PREFIX has to match id_rtid_prepare, and the header is the C++ struct. Versioning either one unilaterally would mean the Rust and C++ sides stop seeing each other's topics, which is the entire purpose of this crate.
Worth noting for anyone reading later: tyndall's C++ rounds each segment up to a page before truncating, while this crate truncates to the exact record size. Any segment the C++ side created is therefore already a full page and comfortably contains the new mapping, and a strict equality check on the size would reject every one of them.
The port of tyndall#18 ("Fix seq_lock cacheline padding and CACHELINE_BYTES on aarch64", issue #16) to the Rust seqlock, so a Rust record and a C++ record are the same bytes again once the drone image's tyndall is bumped past
c7fadae.Changes
CACHELINE_BYTES: 64 on aarch64 (Jetson Orin, Cortex-A78AE — verified on target in the tyndall issue), as on x86_64; armv7 (i.MX, Cortex-A9) keeps 32. Other architectures are acompile_error!instead of a silent guess.entryon its own cacheline: tyndall'salignas(CACHELINE_BYTES) STORAGE entrybecomes a cacheline-aligned wrapper around the entry, so it sits at offsetCACHELINE_BYTESon every ABI regardless of the storage type's alignment. The old[u8; CACHELINE - 4 - size_of::<usize>()]padding assumedseqandsizepack back to back; on 64-bit targets the compiler inserts 4 bytes between them, andentrylanded at 36/40 (aarch64) or 68/72 (x86_64).constassertions over 1-, 4- and 8-byte-aligned storage types (offset_of!(entry) == CACHELINE_BYTES, struct alignment,sizeof, the header offsets), plus a post-monomorphisation guard against an over-aligned storage type (tyndall'sstatic_assert).cargo checkfor a target fails before a mismatching record can be written. The runtime layout test of tyndall'stests/ipc/seq_lock_layout.cppis ported too, over the same storage types.be-node-drone, libblunux#393) bumps its pointer in the same step.entryoffsetCACHELINE_BYTESVerification
cargo test -p ipc_lib(aarch64 macOS, and x86_64 under Rosetta): the layout test over char/float/vec3/mixed/512-byte storage and a write/read round trip.cargo checkforarmv7-unknown-linux-gnueabihf,x86_64-apple-darwinandaarch64-unknown-linux-gnu: theconstlayout assertions hold on all three ABIs. A scratch program over a 128-byte-aligned storage type fails to build onIPC::newalone (the guard), a normal one builds.C++ ↔ Rust interop in Docker against tyndall
origin/main(c83dab3), both directions, five storage types (float,double x,y,z, a mixedlong/double/char/unsigned longstruct,bool, a 512-byte array), run by verification agents on three containers:offsetof(entry)/CACHELINE_BYTESsizeof(seq_lock)small / 512-bytelinux/arm/v7)linux/arm64)linux/amd64)C++-created segments are 4 bytes larger (its type-hash tailer), which C++ also adds when it opens a Rust-created segment; the Rust side ignores it. Shared-memory names agree on both sides. A second read of an unchanged record reports
EAGAIN/EAgainon both sides.Adversarial review (one reviewer, three refuters per finding): two findings confirmed and fixed in the second commit (the alignment guard was not evaluated by
IPC::new;write()did not storesize), three refuted.Not in this PR
One other difference from tyndall's
seq_lockthat a review of the libblunux node surfaced, for a follow-up: the writer's firstseqincrement isReleasewhere tyndall uses a store-store barrier (smp_wmb) — on ARM the entry stores may become visible before the odd sequence number; the fix isAcqRelthere and anAcquirefence between the entry read and the secondseqload inread(). Also not here: the C++ tailer (type hash) is neither written nor checked by the Rust side, as before.🤖 Generated with Claude Code