Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/ptr/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
//! types (`Box`, `Rc`, `Arc`, `Cow`) and `bun_collections` (`TaggedPtr`,
//! `TaggedPtrUnion`). This crate hosts the intrusive/FFI-crossing variants.

// Lets the `::bun_ptr::` paths the ref-count derives emit resolve in-crate.
extern crate self as bun_ptr;

// `bun.ptr.CowSlice(T)` / `CowSliceZ` — the lifetime-free struct port (owns or
// borrows a raw slice with `init_owned`/`borrow_subslice`/`length`). Callers
// that need the struct-shaped API (e.g. `pack_command::Pattern`) reach for
Expand Down
45 changes: 39 additions & 6 deletions src/ptr/ref_count.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ mod tests {

// ── ThreadSafeRefCount (atomic, cross-thread) ─────────────────────────

#[derive(crate::ThreadSafeRefCounted)]
struct Shared {
ref_count: ThreadSafeRefCount<Shared>,
payload: Box<u32>,
Expand All @@ -929,12 +930,12 @@ mod tests {
}
}

impl ThreadSafeRefCounted for Shared {
unsafe fn get_ref_count(this: *mut Self) -> *mut ThreadSafeRefCount<Self> {
// SAFETY: caller contract — pure field projection, no read.
unsafe { &raw mut (*this).ref_count }
}
}
// SAFETY: the count is atomic and `payload` is only ever read, so `&Shared`
// may be used from any thread and the last thread out may run the
// destructor. This is what makes `RefPtr<Shared>: Send + Sync`.
unsafe impl Send for Shared {}
// SAFETY: as above.
unsafe impl Sync for Shared {}

/// `*mut Shared` is not `Send`; the refcount is what makes sharing it sound.
#[derive(Clone, Copy)]
Expand Down Expand Up @@ -976,6 +977,38 @@ mod tests {
assert_eq!(drops(), before + 1);
}

#[test]
fn ref_ptr_clones_cross_threads_and_the_last_one_destroys() {
let _serial = serial();
let before = drops();
let main_ref = RefPtr::new(Shared {
ref_count: ThreadSafeRefCount::init(),
payload: Box::new(5),
});

// Clone through a shared `&RefPtr` on other threads (`Sync`), hand back (`Send`).
let clones: Vec<RefPtr<Shared>> = std::thread::scope(|scope| {
let shared = &main_ref;
let workers: Vec<_> = (0..4)
.map(|_| scope.spawn(move || shared.clone()))
.collect();
workers.into_iter().map(|w| w.join().unwrap()).collect()
});
assert_eq!(main_ref.ref_count.get(), 5);
assert_eq!(drops(), before);

// Release all five refs on concurrent threads: only the count orders the destructor.
let workers: Vec<_> = clones
.into_iter()
.chain(core::iter::once(main_ref))
.map(|theirs| std::thread::spawn(move || *theirs.payload))
.collect();
for worker in workers {
assert_eq!(worker.join().unwrap(), 5);
}
assert_eq!(drops(), before + 1);
}

#[test]
fn thread_safe_release_defers_destruction() {
let _serial = serial();
Expand Down
51 changes: 51 additions & 0 deletions test/internal/rust-ref-ptr-miri.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// `RefPtr<T>` (src/ptr/ref_count.rs) is `Send + Sync` when `T` is. The unit
// test `ref_ptr_clones_cross_threads_and_the_last_one_destroys` is the coverage
// for those impls: five threads release their refs concurrently, so only the
// count's own ordering places the destructor after the other threads' reads,
// and Miri's data-race detector is what checks that. The bun_ptr test binary
// does not link natively, so the crate's tests run under Miri only. This runs
// that test with the same Tree Borrows flags as `bun run rust:miri` and checks
// that it ran and passed. Skipped where miri is not installed or the cargo
// workspace is not resolvable (same prerequisite check as linear-fifo.test.ts).
import { expect, test } from "bun:test";
import { existsSync } from "node:fs";
import path from "node:path";

const cargoBin = Bun.which("cargo");
const repoRoot = path.resolve(import.meta.dir, "..", "..");
const workspaceResolvable =
existsSync(path.join(repoRoot, "vendor", "lolhtml", "Cargo.toml")) &&
existsSync(path.join(repoRoot, "vendor", "rust-argon2", "Cargo.toml")) &&
existsSync(path.join(repoRoot, "build", "debug", "codegen", "build_options.rs"));
const miriAvailable =
!!cargoBin &&
workspaceResolvable &&
Bun.spawnSync({
cmd: [cargoBin, "miri", "--version"],
cwd: repoRoot,
stdout: "ignore",
stderr: "ignore",
timeout: 30_000,
}).exitCode === 0;

test.skipIf(!miriAvailable)(
"RefPtr cross-thread clone and drop is clean under miri's data-race detector",
async () => {
await using proc = Bun.spawn({
cmd: [cargoBin!, "miri", "test", "--locked", "-p", "bun_ptr", "--", "ref_ptr_clones_cross_threads"],
cwd: repoRoot,
env: { ...process.env, MIRIFLAGS: "-Zmiri-tree-borrows", CARGO_TERM_COLOR: "never" },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (exitCode !== 0) {
// Surface miri's diagnostic so the gate/CI log shows the actual UB.
console.error(stderr || stdout);
}
expect(stderr).not.toContain("Undefined Behavior");
expect(stdout).toContain("test ref_count::tests::ref_ptr_clones_cross_threads_and_the_last_one_destroys ... ok");
expect(exitCode).toBe(0);
},
120_000,
);
Loading