Skip to content
Merged
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
15 changes: 14 additions & 1 deletion build/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,10 +651,23 @@ pub(crate) const CONTROLS: &[Control] = &[
red(BLOCKRING, "mutate-no-reissue-after-loss", None, &[
Fails("model::tests::what_a_flush_calls_durable_is_on_the_medium"),
]),
red(TRANSPORT, "publish-relaxed", Some("loom"), &[Fails("a_published_entry_is_read_whole")]),
red(TRANSPORT, "publish-relaxed", Some("loom"), &[
Fails("a_published_entry_is_read_whole"),
Fails("a_published_byte_is_read_as_written"),
]),
red(TRANSPORT, "head-relaxed", Some("loom"), &[Fails("a_published_byte_is_read_as_written")]),
red(TRANSPORT, "no-clamp", Some("loom"), &[Fails(
"a_hostile_producer_yields_entries_or_a_violation",
)]),
red(TRANSPORT, "no-clamp", None, &[Fails(
"stream::tests::a_hostile_producer_hands_the_consumer_at_most_the_ring",
)]),
// The stream's lost wake: a sleeper whose exchange read the waker's clear
// looks at a cursor from before it, and sleeps with nobody to ring.
red(TRANSPORT, "want-relaxed", Some("loom"), &[
Fails("a_consumer_that_looked_again_is_rung_for_bytes_it_missed"),
Fails("a_producer_that_looked_again_is_rung_for_room_it_missed"),
]),
red(TRANSPORT, "end-keeps-inflight", None, &[Fails(
"inflight::tests::an_end_answers_every_tag_once_and_a_late_completion_nothing",
)]),
Expand Down
34 changes: 34 additions & 0 deletions issues/loom-arc-panics-in-its-drop-while-a-failed-model-unwinds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
status: open
kind: tooling
opened: 2026-10-12
---

# Loom's `Arc` panics in its drop while a failed model unwinds

The loom fork, ToyOSOrg/loom `toyos` at `dfe3797`, carries v0.7.2's
`src/sync/arc.rs` unchanged. `Arc`'s `Drop` calls `rt::Arc::ref_dec`, which
branches the scheduler. A model that deadlocks has no thread active when
`Execution::schedule` panics, so the drop that panic's unwinding reaches
unwraps `None` in `rt::thread::Set::active_mut` (`src/rt/thread.rs:276`), and
a destructor panicking during cleanup aborts the process. The test binary
prints loom's `deadlock; threads = ...`, then aborts before the harness prints
the test's `FAILED` line or its summary: a gate that reads the harness's
verdicts finds none. Loom's cells and atomics skip their checks
while `std::thread::panicking()`; its `Arc` does not.

**Evidence** ([ToyOSOrg/evidence `29c8ebcb`, `arc-probe/`](https://github.com/ToyOSOrg/evidence/tree/29c8ebcb04eae318598ff7cb0a045e6e9a1c4f53/arc-probe)):
at `6d2e78400`, with the stream model in
`lib/blockring/transport/tests/loom.rs` on `loom::sync::Arc` in place of
std's, `cargo test -p toyos-transport --features want-relaxed --test loom --
a_consumer_that_looked_again_is_rung_for_bytes_it_missed --exact` kills the
test binary with SIGABRT, `panic in a destructor during cleanup`, through
`<loom::sync::arc::Arc<Stream> as Drop>::drop` and `rt::arc::Arc::ref_dec`.
On std's `Arc` the same run prints the test's `FAILED` line and loom's
`deadlock; threads = ...`.

**Owner**: the orchestrator. Nobody holds it.

**Exit**: a test on the fork's `toyos` branch in which a model that deadlocks
with a `loom::sync::Arc` live prints loom's `deadlock` message and its test
fails without an abort, and that stream model holds its state in loom's `Arc`.
3 changes: 3 additions & 0 deletions lib/blockring/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ impl Word for Shared {
fn store(&self, value: u32, _: Ordering) {
self.0.set(value)
}
fn swap(&self, value: u32, _: Ordering) -> u32 {
self.0.replace(value)
}
}

/// How deep the model's rings are: shallow enough that a script fills and
Expand Down
24 changes: 16 additions & 8 deletions lib/blockring/transport/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# A member of the host workspace (root `Cargo.toml`). The one transport every
# client/server session runs over: the rings, the arena's runs and the tag
# table, as decisions over words an adapter hands in. Nothing here maps, copies
# or blocks.
# client/server session runs over: the rings, the byte streams and their wants,
# the arena's runs and the tag table, as decisions over words an adapter hands
# in. Nothing here maps, copies or blocks.
#
# Its defects are orders and hostile peers, so `tests/loom.rs` holds the
# publication edge and a hostile peer.
# Its defects are orders, lost wakes and hostile peers, so `tests/loom.rs`
# holds the publication edge, the wake handshake and a hostile peer.

[package]
name = "toyos-transport"
version = "0.1.0"
edition = "2021"
description = "The transport every client/server session runs over: rings, an arena of runs and a tag table, decided over words an adapter hands in."
description = "The transport every client/server session runs over: entry rings, byte streams and their wakes, an arena of runs and a tag table, decided over words an adapter hands in."
license = "MIT OR Apache-2.0"
publish = false

Expand All @@ -20,11 +20,19 @@ publish = false
# `CONTROLS` runs each and demands the named test's own FAILED line.
#
# A producer publishes its tail `Relaxed`, so a consumer can see the index
# before the entry's words.
# before the entry's words, or a stream's tail before its bytes.
publish-relaxed = []
# A stream's consumer stores its head `Relaxed` and its producer loads it
# `Relaxed`, so the producer can copy into a byte the consumer is still
# copying out.
head-relaxed = []
# A peer's cursor is believed however far it claims to be, so a hostile
# producer hands the consumer more entries than the ring holds.
# producer hands the consumer more entries, or bytes, than the ring holds.
no-clamp = []
# A stream's waker clears the peer's want `Relaxed`, so a sleeper whose
# exchange reads the clear need not see the cursor stored before it, and
# sleeps over bytes or room nobody rings for.
want-relaxed = []
# `Inflight::end` answers nothing and keeps every tag in flight, so a session's
# requests outlive it unanswered and a late completion answers one.
end-keeps-inflight = []
Expand Down
30 changes: 28 additions & 2 deletions lib/blockring/transport/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
//! **A session is one connection and, at most, one region.** The connection
//! carries the hello, handles, doorbells and the hang-up; the region carries
//! [`Producer`]/[`Consumer`] queues of fixed-size entries and an arena of
//! [`Run`]s. A protocol says what the entries mean; this crate says only what
//! is safe.
//! [`Run`]s, or byte [`stream`]s and the wants that say when to ring. A
//! protocol says what the entries and bytes mean; this crate says only what is
//! safe.
//!
//! **Nothing here holds the region.** An adapter hands every call the `N`
//! [`Word`]s an end was made over, and copies bytes itself, through a run's
Expand All @@ -29,6 +30,7 @@
mod arena;
mod inflight;
mod queue;
pub mod stream;

use core::sync::atomic::{AtomicU32, Ordering};

Expand All @@ -42,6 +44,7 @@ pub use toyos_untrusted::Untrusted;
pub trait Word {
fn load(&self, order: Ordering) -> u32;
fn store(&self, value: u32, order: Ordering);
fn swap(&self, value: u32, order: Ordering) -> u32;
}

impl Word for AtomicU32 {
Expand All @@ -51,6 +54,29 @@ impl Word for AtomicU32 {
fn store(&self, value: u32, order: Ordering) {
AtomicU32::store(self, value, order)
}
fn swap(&self, value: u32, order: Ordering) -> u32 {
AtomicU32::swap(self, value, order)
}
}

/// How an end stores the cursor that hands what it wrote to the peer.
#[cfg(not(feature = "publish-relaxed"))]
const PUBLISH: Ordering = Ordering::Release;
#[cfg(feature = "publish-relaxed")]
const PUBLISH: Ordering = Ordering::Relaxed;

/// Word `at` of an end's words, and the one index into them: every `at` an end
/// forms is below `N`, because its place is and a ring position is reduced
/// below the depth.
#[allow(clippy::indexing_slicing)]
fn word<W, const N: usize>(page: &[W; N], at: usize) -> &W {
&page[at]
}

/// A distance the peer's cursor claims, believed only up to `cap`.
fn clamp(claimed: Untrusted<u32>, cap: u32, broken: Violation) -> Result<u32, Violation> {
let cap = if cfg!(feature = "no-clamp") { u32::MAX } else { cap };
claimed.at_most(u64::from(cap)).ok().and_then(|n| u32::try_from(n).ok()).ok_or(broken)
}

/// The peer wrote what no peer of the protocol writes. The session is over;
Expand Down
21 changes: 1 addition & 20 deletions lib/blockring/transport/src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,7 @@

use core::sync::atomic::Ordering;

use crate::{Untrusted, Violation, Word};

#[cfg(not(feature = "publish-relaxed"))]
const PUBLISH: Ordering = Ordering::Release;
#[cfg(feature = "publish-relaxed")]
const PUBLISH: Ordering = Ordering::Relaxed;

/// Word `at` of an end's words, and the one index into them: every `at` an end
/// forms is below `N`, because its [`Place`] is and a ring position is masked
/// below the depth.
#[allow(clippy::indexing_slicing)]
fn word<W, const N: usize>(page: &[W; N], at: usize) -> &W {
&page[at]
}

/// A distance the peer's cursor claims, believed only up to `cap`.
fn clamp(claimed: Untrusted<u32>, cap: u32, broken: Violation) -> Result<u32, Violation> {
let cap = if cfg!(feature = "no-clamp") { u32::MAX } else { cap };
claimed.at_most(u64::from(cap)).ok().and_then(|n| u32::try_from(n).ok()).ok_or(broken)
}
use crate::{clamp, word, Untrusted, Violation, Word, PUBLISH};

/// Where a queue of `E`-word entries, `D` deep, is among `N` words: the word
/// its consumer stores its head in, the word its producer stores its tail in,
Expand Down
Loading