Skip to content

fix: address Miri UB in Arc, ThinArc, green types, and cursor#211

Draft
avrabe wants to merge 3 commits into
rust-analyzer:masterfrom
pulseengine:fix/miri-soundness-v2
Draft

fix: address Miri UB in Arc, ThinArc, green types, and cursor#211
avrabe wants to merge 3 commits into
rust-analyzer:masterfrom
pulseengine:fix/miri-soundness-v2

Conversation

@avrabe
Copy link
Copy Markdown

@avrabe avrabe commented Apr 6, 2026

Hi — we use rowan in two safety-critical projects (rivet, spar) and ran into the Miri UB documented in #192, #163, #108. We took a shot at fixing it and would appreciate your feedback on the approach.

What this changes

The core issue is that Miri's borrow models (stacked and tree borrows) flag provenance violations when:

  • References to thin types (HeaderSlice<H, [T; 0]>) are used to access data beyond the thin layout
  • &NodeData containing Cell fields freezes the allocation tag, preventing later deallocation
  • Refcount access via Cell::get() or &ArcInner creates references that narrow provenance

The fix touches 4 files (+152/-63 lines):

  • arc.rs: refcount access via raw pointers instead of &ArcInner references; ThinArc clone/drop without transient Arc
  • green/node.rs: GreenNodeData wraps fat Repr (unsized) so provenance covers the full slice; into_raw extracts pointer from ThinArc directly
  • green/token.rs: text() via raw pointer arithmetic; into_raw/deref via ThinArc pointer
  • cursor.rs: SyntaxNode/Token ptr in UnsafeCell; NodeData::self_alloc field preserves original allocation provenance for deallocation; refcount decrement via raw *mut u32

What we DON'T know

  • Performance impact: We haven't benchmarked. The self_alloc field adds 8 bytes per NodeData. The UnsafeCell wrapper and raw pointer access patterns might affect optimization. We'd really appreciate if you could run your benchmarks.
  • Mutable tree path: The clone_for_update tests (ensure_mut_*) still fail under Miri. Our fix focuses on the immutable tree path. The mutable path has additional provenance issues in the singly-linked list infrastructure.
  • Whether this is the right approach: We chose minimal, targeted changes over a larger refactor. There might be cleaner designs (e.g., making the cursor fully raw-pointer-based). We're open to feedback.

Testing

  • cargo test --lib — 6/6 pass
  • cargo +nightly miri test --lib -- --skip ensure_mut — 4/4 pass under -Zmiri-tree-borrows
  • Under stacked borrows, the immutable path still has issues due to the stricter model
  • Tested in downstream: rivet (26 yaml_cst tests pass Miri), spar

Happy to iterate on this based on your feedback. Thank you for maintaining rowan — it's a great library.

avrabe added 2 commits April 5, 2026 23:41
Comprehensive fixes for Miri UB under both stacked and tree borrows:

Arc:
- clone/drop/is_unique: access refcount via ptr::addr_of! on raw pointer
  instead of through &ArcInner reference (avoids provenance narrowing)
- ThinArc::clone/drop: operate on refcount directly via raw pointer
  instead of going through with_arc → transient Arc

Green types:
- GreenNodeData wraps fat Repr (unsized HeaderSlice<H, [T]>) so
  &GreenNodeData has provenance covering the full slice
- GreenNode/GreenToken::deref transmute from fat refs (correct provenance)
- GreenNode/GreenToken::into_raw extract pointer from ThinArc directly
  instead of going through Deref → &Data → NonNull::from
- GreenTokenData::text() uses raw pointer arithmetic for slice access
- thin_to_thick made pub(crate), HeaderSlice fields made pub(crate)

Cursor:
- Cell<NonNull<GreenNodeData>> accessed via as_ptr().read() instead of
  get() to preserve allocation provenance through the Cell

Status: ALL tests pass under -Zmiri-tree-borrows (4/4 non-mutable tests).
Under stacked borrows, only the mutable tree path (clone_for_update)
still fails due to Cell provenance limitations inherent to that model.

Upstream: rust-analyzer#192, rust-analyzer#163, rust-analyzer#108
Make elided lifetimes explicit in return types where &self borrows:
- GreenChild::as_ref() -> GreenElementRef<'_>
- NodeData::green_siblings() -> slice::Iter<'_, GreenChild>

These warnings caused CI failure with RUSTFLAGS="-D warnings".
@avrabe avrabe force-pushed the fix/miri-soundness-v2 branch from 1203c31 to 82c082c Compare April 6, 2026 11:54
@avrabe avrabe force-pushed the fix/miri-soundness-v2 branch from 82c082c to dcbece4 Compare April 6, 2026 12:21
phall1 pushed a commit to phall1/cyrs that referenced this pull request May 10, 2026
… cy-yrz (rowan 0.15→0.16 bump)

cy-h9n (PR #42): workspace-wide rename to all cyrs-*; user-facing
identifiers (cypher-project.toml, // cypher-fmt: off, C ABI, Python
module, LSP language ID, tree-sitter dir, fuzz dict, three binaries)
intentionally kept.

cy-934: 5-year-old upstream UB (issues #163, #108, #192) has an
in-flight competing fix at rust-analyzer/rowan#211 (untouched 1mo, no
reviews). Plan: test #211 against our rehash repro, comment with cyrs
CI links, file complementary PR if #211 doesn't cover the
green/node_cache.rs path.

cy-yrz: latest stable rowan is 0.16.1; we pin "0.15". Port the cy-pom
fix to a 0.16 branch on phall1/rowan, bump cyrs, point patch.crates-io
at it, then drop the cfg(not(miri)) gates from cy-eu2 + cy-863. Blocks
on cy-934 so we can use the upstream branch if it lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@phall1
Copy link
Copy Markdown

phall1 commented May 10, 2026

tested this against a NodeCache rehash repro from cyrs (cypher-frontend). arc/cursor changes here help, but GreenTokenData::text still hits SB UB at src/green/token.rs:104 because only GreenNodeData got promoted to a DST in this PR. GreenToken::deref still hands out a narrow &GreenTokenData whose retag covers only sizeof::<ReprThin>(), so addr_of!(self.data.slice) + from_raw_parts trips:

error: Undefined Behavior: trying to retag from <N> for SharedReadOnly permission at allocM[0x18]
 --> src/green/token.rs:104:25
     let bytes = std::slice::from_raw_parts(slice_start, len);

cyrs CI hits it twice deterministically:

opened #212 with the same DST promotion for tokens. with both stacked the cyrs repro passes. self-contained miri test: https://github.com/phall1/rowan/blob/fix/sb-node-cache-rehash/tests/miri_node_cache_rehash.rs

one more pre-existing SB hit on this branch for the record: ast::tests::ensure_mut_panic_on_create -> GreenNodeData::to_owned -> from_raw -> ThinArc::clone -> count fetch_add. narrow &GreenNodeData doesn't cover the count field. separate fix from #212.

phall1 added a commit to phall1/cyrs that referenced this pull request May 11, 2026
…fixes; drop cy-eu2 miri gates (#43)

* WIP: cy-yrz — rowan 0.16 + patched fork; partial miri gate removal

bumps rowan 0.15 -> 0.16 (zero API breaks observed) and wires
[patch.crates-io] at phall1/rowan branch fix/sb-node-cache-rehash
(stacks rust-analyzer/rowan#211 + our #212; branch is already on
0.16.2, no rebase needed).

removed the cfg(not(miri)) gates on:
- cyrs-fmt::tests::idempotent_around_fmt_off_directive (cy-eu2)
- cyrs-fmt::tests::fixpoint_on_assorted_fixtures (cy-eu2)
both pass under `cargo +nightly miri test -p cyrs-fmt` with the
patched rowan in place. the NodeCache rehash UB those tests were
exposing is fixed by the #211+#212 stack.

removed the cfg(not(miri)) gate on:
- cyrs-plan::lower::tests::lower_statement_no_panic_on_unresolved_inside_patternpredicate_text (cy-863)
this one still fails under miri — but with a *different* SB
violation. not the NodeCache rehash bug; this is in
`rowan::cursor::free` -> `Box::<NodeData>::from_raw` while a
sibling SyntaxNodeChildren iterator still holds a SharedReadOnly
retag of the same allocation. Strongly-protected. Reproduces on
the patched fork; #211+#212 don't touch cursor.rs so this was
never going to fix it.

per cy-yrz instructions: bailing rather than re-adding the gate as
a bandaid. WIP committed for inspection. Need a follow-up bead to
diagnose the cursor::free SB issue (likely needs a third PR
upstream, or a refactor of how cursor refcounts NodeData).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plan): keep cy-863 miri gate, point at cy-208 (cursor::free SB)

Removing the gate exposes a third distinct upstream rowan SB violation
in cursor::free (Box::from_raw races a SyntaxNodeChildren iterator's
retag). Filed cy-208; gate stays until the upstream fix lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(miri): add -Zmiri-disable-isolation so insta snapshot tests can open .snap files

With the rowan SB rehash bug now fixed via the patched fork (this PR),
miri progresses past the SB block and hits the next failure: insta
opens .snap files at runtime, which miri's default isolation rejects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Patrick Hall <phall@macbookpro.mynetworksettings.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants