Skip to content
Draft
52 changes: 36 additions & 16 deletions crates/ffi/src/futex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ pub extern "C" fn kore_futex_available() -> u32 {
/// all waits/wakes. All concurrent accesses must be compatible 32-bit atomic
/// operations (C atomic builtins or Zig atomics); never mix ordinary accesses
/// or differently sized atomics. Managed-thread waits are cancellation points:
/// they may exit without stack unwinding, as specified by kore_thread_testcancel.
/// they may exit without stack unwinding, as specified by kore_thread_testcancel,
/// and a cancellation requested while the thread waits wakes it to act.
/// Publish the condition with a sequentially consistent atomic store BEFORE waking.
/// No address-space sharing is implied.
#[unsafe(no_mangle)]
Expand All @@ -77,32 +78,51 @@ pub unsafe extern "C" fn kore_futex_wait(
if !crate::thread_local::cancellable() {
return futex::wait(word, expected, timeout).into();
}
// A bounded native park makes cancellation observable even without a peer
// kick capability. Recompute against one deadline; slices never restart it.
unsafe { crate::kore_thread_testcancel() };
let start = match kore::time::uptime() {
Ok(time) => time.as_nanos(),
Err(_) => return KoreFutexOutcome::Unsupported,
let outcome = match crate::thread::slice() {
// A cancellation's wake reaches this thread (kore_thread_cancel), and
// the wait re-reads the request once it is registered: it parks until
// a wake, the caller's deadline or that request, and never polls.
None => futex::wait_unless(word, expected, timeout, crate::thread_local::cancel_pending),
Some(slice) => unsafe { sliced(word, expected, timeout_ns, slice) },
};
let deadline = timeout.map(|_| start.saturating_add(timeout_ns));
unsafe { crate::kore_thread_testcancel() };
outcome.into()
}

/// A cancellable wait on a thread the controller cannot wake: no wake would
/// announce a cancellation, so it parks one `slice` at a time and acts on a
/// request between slices, against one deadline that slices never restart.
/// # Safety
/// A managed thread's cancellation point, as kore_futex_wait.
unsafe fn sliced(
word: &AtomicU32,
expected: u32,
timeout_ns: u64,
slice: Duration,
) -> futex::WaitOutcome {
let Ok(start) = kore::time::uptime() else {
return futex::WaitOutcome::Unsupported;
};
let deadline =
(timeout_ns != KORE_FUTEX_INFINITE).then(|| start.as_nanos().saturating_add(timeout_ns));
let slice = u64::try_from(slice.as_nanos()).unwrap_or(u64::MAX);
loop {
unsafe { crate::kore_thread_testcancel() };
let now = match kore::time::uptime() {
Ok(time) => time.as_nanos(),
Err(_) => return KoreFutexOutcome::Unsupported,
let Ok(now) = kore::time::uptime() else {
return futex::WaitOutcome::Unsupported;
};
let slice = crate::thread_local::CANCEL_SLICE_NS;
let slice = deadline.map_or(slice, |end| end.saturating_sub(now).min(slice));
let outcome = futex::wait(word, expected, Some(Duration::from_nanos(slice)));
let bound = deadline.map_or(slice, |end| end.saturating_sub(now.as_nanos()).min(slice));
let outcome = futex::wait(word, expected, Some(Duration::from_nanos(bound)));
unsafe { crate::kore_thread_testcancel() };
if outcome != futex::WaitOutcome::TimedOut {
return outcome.into();
return outcome;
}
if let Some(end) = deadline {
match kore::time::uptime() {
Ok(time) if time.as_nanos() < end => {}
Ok(_) => return KoreFutexOutcome::TimedOut,
Err(_) => return KoreFutexOutcome::Unsupported,
Ok(_) => return futex::WaitOutcome::TimedOut,
Err(_) => return futex::WaitOutcome::Unsupported,
}
}
}
Expand Down
87 changes: 78 additions & 9 deletions crates/ffi/src/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ pub(crate) struct Control {
outcome: AtomicU32,
retired: AtomicU32,
pub(crate) cancel: AtomicBool,
/// The native band the thread runs on, plus one; zero until it starts
/// (`futex::current_thread`). A canceller reads it after storing
/// `cancel`, to wake the thread out of a wait at a cancellation point.
band: AtomicU32,
}
impl Control {
pub(crate) fn new() -> Self {
Expand All @@ -53,13 +57,32 @@ impl Control {
outcome: AtomicU32::new(0),
retired: AtomicU32::new(0),
cancel: AtomicBool::new(false),
band: AtomicU32::new(0),
}
}
/// Record the calling thread's band, before its callback can reach a
/// cancellation point. `SeqCst`, against the canceller's store of `cancel`
/// and then this load: a canceller that reads zero stored `cancel` before
/// the thread's first wait reads it.
fn start(&self) {
let band = kore::futex::current_thread().map_or(0, |band| band as u32 + 1);
self.band.store(band, Ordering::SeqCst);
}
fn band(&self) -> Option<usize> {
let band = self.band.load(Ordering::SeqCst);
band.checked_sub(1).map(|band| band as usize)
}
pub(crate) fn finish(&self, value: *mut c_void, outcome: u32) {
self.result.store(value, Ordering::Relaxed);
self.outcome.store(outcome, Ordering::Release);
}
}
/// How long one wait of the calling thread may park ([`Controller::slice`]):
/// unbounded where the controller can wake it, and therefore where a
/// cancellation's wake reaches it; one bounded slice where nothing could.
pub(crate) fn slice() -> Option<core::time::Duration> {
THREADS.slice()
}
fn status(error: Error) -> i32 {
match error {
Error::Unavailable | Error::Full => KORE_THREAD_EAGAIN,
Expand All @@ -86,6 +109,7 @@ fn invoke(argument: usize) -> usize {
call_context(context)
}
fn call_context(context: &StartContext) -> usize {
context.control.start();
// SAFETY: forwarded foreign callback and argument lifetime contract.
crate::thread_local::enter(&context.control);
let value = unsafe { (context.call)(context.argument) };
Expand Down Expand Up @@ -208,12 +232,17 @@ pub unsafe extern "C" fn kore_thread_join(
if control.retired.load(Ordering::SeqCst) != 0 {
break;
}
// A cancellation request is a flag, not a wake: a joiner that can be
// cancelled parks one slice at a time, as a cancellable futex wait
// does. Any other joiner parks until the controller retires the target.
let slice = crate::thread_local::cancellable()
.then(|| core::time::Duration::from_nanos(crate::thread_local::CANCEL_SLICE_NS));
if kore::futex::wait(&control.retired, 0, slice) == kore::futex::WaitOutcome::Unsupported
// Park until the controller wakes `retired`, or this joiner's own
// cancellation is requested: its canceller stores the request, then
// wakes this thread (kore_thread_cancel), and the wait re-reads the
// request once it is registered. Only a joiner the controller cannot
// wake parks in bounded slices; nothing else would end its park.
if kore::futex::wait_unless(
&control.retired,
0,
slice(),
crate::thread_local::cancel_pending,
) == kore::futex::WaitOutcome::Unsupported
// A native worker with no doorbell takes the deadline park, which
// needs no wake capability at all.
&& !kore::runtime::pace::cede()
Expand Down Expand Up @@ -270,7 +299,12 @@ pub unsafe extern "C" fn kore_thread_detach(thread: *mut KoreThread) -> i32 {
}

/// Request deferred cancellation. The target acts at testcancel, join or foreign
/// futex wait. A disabled target retains the request. No asynchronous kill occurs.
/// futex wait; a target waiting at one is woken to act. A disabled target
/// retains the request. No asynchronous kill occurs. EAGAIN when no wake the
/// calling thread sends can reach the target, because it neither started the
/// controller nor was created through it (create refuses it too). Then no
/// request is recorded, since a target parked at a cancellation point would
/// never see it.
/// # Safety
/// thread is a live handle. Borrowing cancel calls may run concurrently with one
/// another, but must finish before a consuming join/detach can free the handle.
Expand All @@ -279,12 +313,31 @@ pub unsafe extern "C" fn kore_thread_cancel(thread: *mut KoreThread) -> i32 {
let Some(ticket) = NonNull::new(thread.cast::<Ticket>()) else {
return KORE_THREAD_EINVAL;
};
let control = unsafe { &ticket.as_ref().control };
// SAFETY: the caller supplies a live handle, borrowed for this call.
request_cancel(unsafe { &ticket.as_ref().control })
}

/// kore_thread_cancel's request against the target's shared control.
fn request_cancel(control: &Control) -> i32 {
if control.outcome.load(Ordering::Acquire) != 0 || control.retired.load(Ordering::Acquire) != 0
{
return KORE_THREAD_ESRCH;
}
control.cancel.store(true, Ordering::Release);
// The request is a flag its wake announces: only the controller holds a
// send into a managed thread's doorbell, so the wake is relayed through
// it, and a thread that cannot relay through it is refused here rather
// than leaving a request behind a park nothing ends.
if !THREADS.wakes_threads() {
return KORE_THREAD_EAGAIN;
}
// Store, then wake (`futex::wake_thread`'s order). The wake reads the
// target's registration, and the target's wait reads the request once it
// is registered, so one of the two sees the other. A target that has not
// yet recorded its band reads the request at its first cancellation point.
control.cancel.store(true, Ordering::SeqCst);
if let Some(band) = control.band() {
let _ = kore::futex::wake_thread(band);
}
KORE_THREAD_OK
}

Expand Down Expand Up @@ -313,6 +366,22 @@ mod tests {
assert_eq!(Arc::strong_count(&control), 1);
}
#[test]
fn a_cancel_no_wake_can_announce_is_refused_and_never_recorded() {
// Off metal the controller never starts, so no thread's wake reaches
// its threads: the request is refused, and nothing a parked target
// would wait on unwoken is left behind. An ended target is gone first.
let control = Control::new();
control.start();
assert_eq!(control.band(), None, "off metal a thread has no band");
assert_eq!(request_cancel(&control), KORE_THREAD_EAGAIN);
assert!(!control.cancel.load(Ordering::SeqCst));
control.finish(ptr::null_mut(), 1);
assert_eq!(request_cancel(&control), KORE_THREAD_ESRCH);
assert!(!control.cancel.load(Ordering::SeqCst));
// Every wait off metal is one the controller cannot end early.
assert!(slice().is_some());
}
#[test]
fn host_refusal_preserves_output_and_never_calls_foreign_code() {
unsafe extern "C" fn never(_: *mut c_void) -> *mut c_void {
panic!("host ran native callback")
Expand Down
18 changes: 11 additions & 7 deletions crates/ffi/src/thread_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ pub const KORE_THREAD_CANCEL_ENABLE: i32 = 0;
pub const KORE_THREAD_CANCEL_DISABLE: i32 = 1;
pub const KORE_THREAD_CANCEL_DEFERRED: i32 = 0;
pub const KORE_THREAD_CANCEL_ASYNCHRONOUS: i32 = 1;
/// How long one park lasts at a cancellation point that waits: a request is
/// a flag no wake carries, so a cancellable wait re-checks it this often.
/// One millisecond is the cadence the native runtime parks an unreachable
/// waiter at (`kore-rt`'s `pace::CEDE_NS`): a cancellation is acted on within
/// one, and an idle cancellable waiter wakes no more often than that.
pub(crate) const CANCEL_SLICE_NS: u64 = 1_000_000;
pub type KoreThreadKey = u64;
pub type KoreThreadDestructor = Option<unsafe extern "C" fn(*mut c_void)>;

Expand Down Expand Up @@ -85,6 +79,16 @@ pub(crate) fn leave() {
pub(crate) fn cancellable() -> bool {
!CURRENT.get().is_null() && !DISABLED.get() && !FINISHING.get()
}
/// An enabled cancellation request is pending for this managed thread: what
/// testcancel acts on, and the stop a cancellation point's wait re-reads once
/// it is registered (`futex::wait_unless`). `SeqCst`, as the canceller's store
/// is: the canceller stores, then reads this thread's registration before it
/// wakes it, so one of the two sees the other.
pub(crate) fn cancel_pending() -> bool {
// SAFETY: CURRENT is non-null while cancellable, and points at this
// managed thread's Control until `leave`.
cancellable() && unsafe { &*CURRENT.get() }.cancel.load(Ordering::SeqCst)
}
fn active() -> bool {
!CURRENT.get().is_null()
}
Expand Down Expand Up @@ -368,7 +372,7 @@ pub unsafe extern "C" fn kore_thread_setcanceltype(kind: i32, old_type: *mut i32
/// never cross live Rust RAII guards or executor callbacks at a cancellation point.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn kore_thread_testcancel() {
if cancellable() && unsafe { &*CURRENT.get() }.cancel.load(Ordering::Acquire) {
if cancel_pending() {
unsafe {
kore_thread_exit(kore_thread_canceled());
}
Expand Down
116 changes: 116 additions & 0 deletions docs/provenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,119 @@ C program (`docs/languages.md`). It is the first libkore package a C program
on Kore links without a Rust host. Its shared object is a distribution's
one-line `cdylib` over it. `tools/check-program.py` links that object for the
three Kore ISAs and checks its shape and its exact exported ABI.

## A cancellation is a wake

The export records Kore `a4ab7234e2fe60b43a1df42f12be8f636255fc7a`, one commit
past MicroPerceptron/kore#1494's branch head `76fddb9f`. Two of the six
commits since `0018956a` reach the closure, which keeps its 26 packages and
382 files:

- `kore-rt`'s futex gains a wait another thread can end, and the wake that
ends it (Kore `a4ab7234`). `futex::wait_unless` is `wait` with a caller
condition read once the waiter is registered, as the word is, and always
parks on the doorbell. `futex::wake_thread` kicks one thread's doorbell
rather than a word's waiters, directly or relayed through the caller's hub,
and reads the target's registration before it kicks. A condition stored
with `SeqCst` before that wake is therefore never missed, and no slice is
needed to notice it. Arm, register and check are one function that every
doorbell wait shares, and the host suite drives it with the arm injected to
pin that order. `futex::current_thread` is public: the band a thread
records for its wakers.
- Its thread controller exposes the rule its own joins used:
`Controller::slice` is unbounded where the controller can wake the calling
thread, and `UNREACHED_SLICE` where nothing could. `Controller::wakes_threads`
says whether a wake the calling thread sends reaches the threads it founded.
- The services' `boot.toml` digests are #1494's reseal over the parking
controller (`76fddb9f`).

No exported manifest changed, so `native/Cargo.lock` did not.

`kore-ffi`'s cancellation points no longer poll. A managed thread records its
band when it starts; `kore_thread_cancel` stores the request and wakes that
band; `kore_thread_join` and a managed thread's `kore_futex_wait` wait with
`wait_unless`, with no timeout of their own. An idle cancellable waiter now
sleeps until something happens rather than waking every millisecond. Only a
thread the controller cannot wake keeps the bounded slice, now the
controller's `UNREACHED_SLICE`. A canceller whose wake cannot reach the
controller's threads is refused with EAGAIN, as create refuses it, and records
nothing. A host test drives `request_cancel` to that refusal. The metal
evidence is Kore's `keytest-tcc-x86`, whose libkore tour cancels a thread
parked in `pthread_join` and one parked in `kore_futex_wait` with no timeout.

The export then moved to Kore `40de25825eb1ea7816f7e7df86e116b3ad20d1fc`,
which closes a child's founding window. The controller sets a child's reach
bit only after `spawn_with_arg` returns, and the child ran its callback at
once. Its first wait could therefore start before the controller had
recorded that it can kick the child. A cancellable `kore_futex_wait` in that
window took the unreached slice and kept it until its first real wake.
`child_entry` now settles on its record until the founding is published,
which follows the reach bit, so a thread the controller founds with a
doorbell is reached from its first wait. A host test holds a founding back
and checks that the callback waits for it. The closure keeps its 26 packages
and 382 files. Besides `thread_controller.rs`, it carries the
`services/swap/boot.toml` digest of Kore's reseal over the cancellation wake
(`bfecfd17`).

The export then moved to Kore `618772900e088b8b45ebafa8101d066edf4ffb3b`,
the merge of MicroPerceptron/kore#1494's newer branch into this change. That
branch brings three changes, which its own exports carried here up to Kore
`2d4998ba`:

- threads that found take a founder's arena;
- a founding's staged space waits for the reap;
- Kore's C library (`kore-c-shim`).

The closure is now 27 packages and 401 files. Against that branch's export,
only kore-rt's `futex.rs` and `thread_controller.rs` and the record differ.
The two sides' controller changes compose: the controller and its children
are founded with `spawn_founder_with_arg`, and a child still waits for its
founding before its callback runs.

It then moved to Kore `4a56f8279c79fd4d65918e581fe382f0775dfb51`, the merge
of #1494's next round: kore-rt names the link window's free span once, and C
programs gain descriptors and mmap. The closure stays at 27 packages and 401
files. Against that branch's export at `10c7b400`, only kore-rt's `futex.rs`
and `thread_controller.rs`, the record, and five services' `boot.toml`
digests differ. The digests come from #1494's own reseal (`f2644e5b`), which
followed its export.

It then moved to Kore `eb59823a29cc9b0cfe0ea27a386c3fac266534ef`, the merge
of #1494's merge of Kore main `f2dc8309`. Main reaches the closure in four
places:

- the seat clipboard and Cmd chords (#1469): kore-abi's insert into a held
input line and a window's clipboard chunks, and kore-window's clipboard,
key split and shortcuts;
- Files' live folder and sidebar (#1450, #1456): a DagFS watch plane in the
`dagfs` crate and service, and kore-rt's owed service wakes
(`SystemService::take_owed_wakes`), which the serve host delivers after
each unit of work;
- DagFS's cwd anchor and `MatchView`, `.` and `..` in `resolve`, and a dead
view reaped by asking the kernel;
- five services' `boot.toml` digests, from #1494's reseal over the merge
(`e6b108d1`).

The closure is now 27 packages and 406 files. Against Kore `e6b108d1`, only
kore-rt's `futex.rs` and `thread_controller.rs` differ. No exported manifest
changed, so `native/Cargo.lock` did not.

It then moved to Kore `5c09f155b03682e95f24f924cdd65188fc9ebc60`, the merge
of #1494's next round into this change. That round lets the kernel observe a
PCI function's live state and its BARs' shapes, and puts the name a program
was run by in its process page. libkore#32 exported it first, at Kore
`dfece644`. The closure is now 27 packages and 407 files. Against Kore
`4b236fbe`, only kore-rt's `futex.rs` and `thread_controller.rs` differ.
Against libkore#32's export, five services' `boot.toml` digests differ as
well, from #1494's reseals that followed it. No exported manifest changed,
so `native/Cargo.lock` did not.

It then moved to Kore `7c0be6e84ac2e8ee67591b44c0f42d46ab596f83`, the merge
of #1494's next round into this change. That round has the kernel offer a
function to drivers only on positive evidence and name each claim's holder,
which kore-abi's device state and its new device classes carry. The closure
is now 27 packages and 408 files. Against Kore `fe9603e3`, only kore-rt's
`futex.rs` and `thread_controller.rs` differ, and `services/swap/boot.toml`:
swap links kore-rt's futex, so its digests are this change's reseal over
the ioctl round (`b04325e9`). No exported manifest changed, so
`native/Cargo.lock` did not.
Loading
Loading