Skip to content

ffi: a cancellation wakes its target, and no cancellable wait polls - #33

Draft
SnowCheetos wants to merge 8 commits into
ft/fervent-shannon-egp5ptfrom
ft/brave-heisenberg-zgoaux
Draft

SnowCheetos wants to merge 8 commits into
ft/fervent-shannon-egp5ptfrom
ft/brave-heisenberg-zgoaux

Conversation

@SnowCheetos

@SnowCheetos SnowCheetos commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A cancellation request now wakes its target, and no cancellable wait polls.
Before, kore_thread_cancel only set a flag that nothing woke the target
for, so a managed thread's cancellable kore_thread_join or
kore_futex_wait parked in 1 ms slices (CANCEL_SLICE_NS) to re-read it.
An idle cancellable waiter woke a thousand times a second.

The native half lands in Kore first, as AGENTS.md requires. It is
MicroPerceptron/kore#1534, stacked on MicroPerceptron/kore#1494, and
arrives here through tools/sync-native.py. This PR is stacked on
#32 (ft/fervent-shannon-egp5pt, the libkore half of
MicroPerceptron/kore#1494), which adds kore-c and the parking thread
controller this builds on.

What

  • native: re-export at Kore a4ab7234: a byte-exact export. Two of the
    six Kore commits since 0018956a reach the closure, which keeps its 26
    packages and 382 files:

    • a4ab7234, this change's kore-rt half:
      • futex::wait_unless, a wait whose caller condition is read once the
        waiter is registered;
      • futex::wake_thread, which kicks one thread's doorbell and reads its
        registration first;
      • futex::current_thread, now public;
      • the controller's Controller::slice, Controller::wakes_threads and a
        public UNREACHED_SLICE.
    • 76fddb9f, MicroPerceptron/kore#1494's reseal, which moves five
      services' boot.toml digests.

    native/Cargo.lock is unchanged.

  • ffi: a cancellation wakes its target:

    • A managed thread records its band when it starts.
    • kore_thread_cancel stores the request (SeqCst) and then wakes that
      band, relayed through the controller, the one owner of a send into
      each managed thread's doorbell.
    • kore_thread_join and a managed thread's kore_futex_wait wait with
      wait_unless and set no timeout of their own. CANCEL_SLICE_NS is
      gone.
    • Only a thread the controller cannot kick still parks in bounded
      slices, now the controller's own rule. That also bounds a joiner the
      controller cannot reach, which before parked with no bound and no wake.
    • A canceller whose wake cannot reach the controller's threads (one that
      neither started the controller nor was created through it) is refused
      with KORE_THREAD_EAGAIN and records nothing, as create refuses it. A
      request a parked target never saw would be a silent loss. In a C
      program, the only such caller is thread 0 running a kore_fs_run step.
    • kore.h changes in doc comments only; the 85-symbol ABI is the same.
    • docs/runtime.md says how a cancellation reaches a parked thread, and
      drops two statements the parking controller had made stale.
      docs/provenance.md records the export.
  • native: re-export at Kore 40de2582: closes a thread's founding
    window. The controller sets a child's reach bit only after
    spawn_with_arg returns, and child_entry ran the callback at once. A
    cancellable kore_futex_wait the child entered before that bit took
    Controller::slice's unreached bound, and sliced kept it until the
    wait's first real wake. child_entry now settles on its record until the
    founding is published, which follows the reach bit. A thread the
    controller founds with a doorbell is therefore reached from its first
    wait, and only a thread it cannot wake keeps the slice.

    • Byte-exact. Two Kore commits since a4ab7234 reach the closure, still
      26 packages and 382 files: 40de2582's thread_controller.rs, and the
      services/swap/boot.toml digest of Kore's reseal over the
      cancellation wake (bfecfd17). native/Cargo.lock is unchanged.
    • docs/provenance.md records the export. docs/runtime.md says why a
      founded thread never takes the slice.
  • A merge of c: libkore.so, the runtime and host of an independent C program #32's newer branch (0460f5c),
    which brings three native re-exports (founder arenas, a founding's
    staged space waiting for the reap, Kore's C library) and stdio for C
    programs.

    • Both sides changed native/, which is never merged by hand.
      tools/sync-native.py re-exports it at Kore 61877290, the merge of
      #1494's branch into MicroPerceptron/kore#1534's. The closure is now 27
      packages and 401 files.
    • Against #1494's export, only kore-rt's futex.rs and
      thread_controller.rs and the record differ.
    • Everything outside native/ merges cleanly.
  • A merge of libkore#32's next round (98186bf): descriptors and mmap
    over the capabilities a C program holds, kore-rt naming the link window's
    free span once (exported at Kore 10c7b400), and rustix 1.1.6.

    • native/ is re-exported at Kore 4a56f827, the merge of #1494's next
      round into MicroPerceptron/kore#1534's. It is still 27 packages and 401
      files.
    • Against #1494's export, 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 reseal f2644e5b, which
      followed its export.
    • Everything outside native/ merges cleanly.
  • native: re-export at Kore eb59823a: Kore's #1494 merged main
    f2dc8309 and resealed over it (e6b108d1), and
    MicroPerceptron/kore#1534 merges that at eb59823a. libkore#32's branch
    had not moved, so this commit is a re-export alone.

    • Main reaches the closure through the seat clipboard and Cmd chords
      (kore-abi, kore-window), Files' DagFS watch plane and kore-rt's owed
      service wakes, and DagFS's cwd anchor and path folding. Five services'
      boot.toml digests come from #1494's reseal over the merge.
    • The closure is now 27 packages and 406 files. Against Kore e6b108d1,
      only kore-rt's futex.rs and thread_controller.rs differ.
      native/Cargo.lock is unchanged.
    • docs/provenance.md records the export.
  • A merge of libkore#32's arguments round (2ce41f1): a C program's
    main receives its arguments (crates/c), and libkore#32's export at
    Kore dfece644, where the process page carries the name a program was
    run by.

    • native/ is re-exported at Kore 5c09f155, the merge of #1494's
      round into MicroPerceptron/kore#1534's: 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 from #1494's later reseals differ too.
    • Everything outside native/ merges cleanly.
  • native: re-export at Kore 7c0be6e8: #1494's platform round, where
    the kernel offers a function to drivers only on positive evidence and a
    claim names its holder. MicroPerceptron/kore#1534 merges it at
    7c0be6e8. libkore#32's branch has not moved, so this commit is a
    re-export alone.

    • kore-abi's device state changes and gains its device classes. The
      closure is 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 MicroPerceptron/kore#1534's
      reseal over the ioctl round.
    • native/Cargo.lock is unchanged. docs/provenance.md records the
      export.

Why the order is lossless

The canceller stores, then reads the target's registration, before it
kicks or relays. The relaying controller's sweep reads it again before it
kicks. The target registers, then reads the request. All of these accesses
are SeqCst, so one of the two sides sees the other:

  • A canceller that misses the registration stored before the target's
    read, so the target does not park.
  • A canceller that sees the registration kicks a post that already stands,
    because the target armed before it registered. That kick resolves the
    park.

Kore's host suite pins the stop-after-registration order. One test checks
the order directly, and another runs the writer at each of the five points
it can meet a waiter. With the stop read moved before the registration,
both fail, the second with the lost wake itself.

Verification

On this head (cbe7b0a), AGENTS.md's set passes:
make fmt test check docs owners cross miri bindings-check.

  • make test: 674 Rust tests pass and none fail, and the 7 tool tests
    pass. The 10 ignored are kore-rt's 6 ignore doc examples and kore-vol's
    4 loop-device tests, which need root. kore-rt's suite (471) runs here
    through the export, including:

    • futex::a_stop_is_read_only_once_the_waiter_is_registered and
      futex::a_stop_published_before_its_wake_is_never_missed, the order
      tests;
    • futex::off_metal_a_stoppable_wait_refuses_before_reading_its_stop;
    • futex::a_thread_wake_kicks_only_a_registered_waiter_and_relays_a_refusal;
    • thread_controller::an_unstarted_controller_wakes_no_thread_and_bounds_every_wait;
    • thread_controller::a_child_runs_its_callback_only_once_its_founding_is_published,
      which fails with the founding wait removed.

    kore-ffi's suite (31) includes
    thread::a_cancel_no_wake_can_announce_is_refused_and_never_recorded.

  • make check: clippy with -D warnings, and tools/check-native.py:
    the export is the exact recorded bytes of Kore 7c0be6e8.

  • make docs: rustdoc with -D warnings.

  • make cross: the Kore user targets for x86_64, aarch64 and riscv64.
    tools/check-program.py links libkore.so on each: 130 exported
    symbols, the declared ABI exactly. The added symbols are #1494's: Kore's
    C library, descriptors and mmap, and __kore_program_arguments.

  • make miri: 94 tests.

  • make owners and make bindings-check: kore.h regenerates
    exactly, and its C and Zig declarations check on every ISA.

  • This head passes this repository's CI (run 43163880067): host, miri,
    and targets for x86_64, aarch64 and riscv64gc. So did a940932 and
    33ba95c before it (runs 43151521652 and 43139759911).

    • On c839547 before them, the three targets jobs died at
      sudo apt-get install -y llvm, because the hosted runner could not
      resolve Ubuntu's mirrors (see the comment below). Every check before
      that step passed.
    • The same workflow passed unchanged on the next head, so that failure
      was the runners', not this PR's.

MicroPerceptron/kore#1534 pins this head. There, make keytest-tcc-x86
passes on the resealed production image, run with a 1200 s session budget
because #1494's new steps carry the session past 600 s under TCG (that PR
says how):

  • The libkore tour is built by tcc at Kore's shell against this
    revision's libkore.so.
  • It runs #1494's steps first: two dozen threads alive at once, and an SPMD
    sum. The keytest also runs #1494's hello.c and memory.c through the C
    library, and its args program, whose main receives its name and the
    words of its command line.
  • It checks that the two threads it is about to cancel can park
    (CANCEL_PARKS, exit 28). The controller therefore reached each one
    before its callback ran, and neither wait has a slice.
  • It cancels the thread parked in pthread_join, then the one parked in
    kore_futex_wait with no timeout, on a word nothing wakes. Each joins
    with PTHREAD_CANCELED within a second, and the tour ends with
    exit 42.

run-boot-gate-x86 passes there too: 19/19 services founded. That PR lists
the rest of Kore's image matrix. This repository's checks are compiler,
host and Miri evidence, not boot evidence.

Not run: tools/check-thread-guest.py. Kore fc2a5685 reshaped hello's
entry, so the runner's injection hook no longer exists there, and neither
does the ExitCode::app its replacement calls. The runner needs its own
update.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ACHC7hXQ3xRtyu6Yt31jXQ

…wake

## Why

kore-ffi's cancellable waits slice their parks, because no wake names a
thread. Kore `a4ab7234` adds the wait and the wake that remove the slice.
They arrive here through the exporter rather than as edits to the snapshot.

## What

- `native/` is re-exported byte for byte at
  `a4ab7234e2fe60b43a1df42f12be8f636255fc7a` with `tools/sync-native.py`,
  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`: `futex::wait_unless`, a wait another thread can end;
    `futex::wake_thread`, the wake that ends it; and `futex::current_thread`,
    now public. The thread controller adds `Controller::slice`,
    `Controller::wakes_threads`, and a public `UNREACHED_SLICE`.
  - The services' `boot.toml` digests, #1494's reseal over the parking
    controller.
- No exported manifest changed, so `native/Cargo.lock` did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACHC7hXQ3xRtyu6Yt31jXQ
A cancellation request was a flag that no wake carried, so a cancellable
`kore_thread_join` or managed-thread `kore_futex_wait` parked one
millisecond at a time to notice it. An idle cancellable waiter woke a
thousand times a second.

- A managed thread records its band when it starts. `kore_thread_cancel`
  stores the request (`SeqCst`, was `Release`) and then wakes that band
  with `futex::wake_thread`. The wake is relayed through the controller,
  the one owner of a send into each managed thread's doorbell.
- `kore_thread_join` and a managed thread's `kore_futex_wait` wait with
  `futex::wait_unless`. It reads the request again once the waiter is
  registered, and the wake reads that registration, so neither misses the
  other. Neither wait sets a timeout of its own. `CANCEL_SLICE_NS` is gone.
- Only a thread the controller cannot wake parks in bounded slices, now
  the controller's own rule and cadence (`Controller::slice`,
  `UNREACHED_SLICE`). That also bounds a joiner the controller cannot
  reach, which before parked with no bound and no wake.
- A canceller whose wake cannot reach the controller's threads is refused
  with EAGAIN and records nothing, as create refuses it: a thread that
  neither started the controller nor was created through it. A request a
  parked target never saw would be a silent loss. A host test drives the
  refusal.
- `kore.h` is regenerated with doc changes only, so the ABI is unchanged.
  `docs/runtime.md` says how a cancellation reaches a parked thread, and
  drops two statements the parking controller had made stale: a 1 ms
  controller scan, and controller threads without doorbells.
  `docs/provenance.md` records the export. The thread guest fixture's
  comment describes both ways a cancellable worker waits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACHC7hXQ3xRtyu6Yt31jXQ
… first wait

Kore `40de2582` closes a managed thread'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. A cancellable `kore_futex_wait` the child
entered before that bit took `Controller::slice`'s unreached bound, and
`sliced` kept it until the wait's first real wake. `child_entry` now
settles on its record until its founding is published, which follows the
reach bit. A thread the controller founds with a doorbell is therefore
reached from its first wait, and only a thread it cannot wake keeps the
slice.

The export is byte-exact. Two commits since `a4ab7234` reach the closure,
which keeps its 26 packages and 382 files: `40de2582`'s
`thread_controller.rs`, and the `services/swap/boot.toml` digest of
Kore's reseal over the cancellation wake (`bfecfd17`). No manifest
changed, so `native/Cargo.lock` did not. `docs/provenance.md` records the
export, and `docs/runtime.md` says why a founded thread never takes the
slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACHC7hXQ3xRtyu6Yt31jXQ
`ft/fervent-shannon-egp5pt` moved to `0460f5c`. It carries three native
re-exports, at Kore `2a0730dd`, `6c57f344` and `2d4998ba`: founder arenas
for founding threads, a founding's staged space waiting for the reap, and
Kore's C library. It also carries stdio for C programs.

Both sides changed `native/`, which is never merged by hand. It is
re-exported here by `tools/sync-native.py` at Kore `61877290`, the merge of
#1494's branch into this change's. The closure is now 27 packages and 401
files. Against #1494's export, only kore-rt's `futex.rs` and
`thread_controller.rs` and the record differ. Everything outside `native/`
merges cleanly, and `docs/provenance.md` records the export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACHC7hXQ3xRtyu6Yt31jXQ
`ft/fervent-shannon-egp5pt` moved to `98186bf`, with three changes:

- a native re-export at Kore `10c7b400`, where kore-rt names the link
  window's free span once;
- descriptors and mmap over the capabilities a C program holds;
- rustix 1.1.6.

`native/` is re-exported by `tools/sync-native.py` at Kore `4a56f827`, the
merge of #1494's branch into this change's. The closure stays at 27
packages and 401 files. Against #1494's export, 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 reseal `f2644e5b`, which
followed its export. Everything outside `native/` merges cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACHC7hXQ3xRtyu6Yt31jXQ

Copy link
Copy Markdown
Contributor Author

targets (x86_64), targets (aarch64) and targets (riscv64gc) failed on c839547, twice. The failure is not this PR's.

  • Each job dies in Verify freestanding C allocation symbols, at sudo apt-get install -y llvm. The hosted runner cannot resolve archive.ubuntu.com, security.ubuntu.com or azure.archive.ubuntu.com (Temporary failure resolving). Every check before that step passed: cargo check for each target, and the linked returning programs.
  • The re-run of the failed jobs (attempt 2, 16:00 UTC) failed the same way. host and miri pass.
  • This PR does not touch the workflow or tools/check-alloc-symbols.py. The same jobs passed on c: libkore.so, the runtime and host of an independent C program #32 at 13:12 UTC, before the runners lost those names. Locally, this head's make cross runs check-alloc-symbols.py, and it passes.

No fix exists in the repository yet. The step could stop depending on Ubuntu's mirrors by taking llvm-nm from the Rust toolchain instead: rustup component add llvm-tools, with LLVM_NM=$(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-nm, which the script already reads. That change belongs with the workflow, not this one.


Generated by Claude Code

Kore's #1494 merged main `f2dc8309` and resealed over it (`e6b108d1`).
MicroPerceptron/kore#1534 merges that into the cancellation wake at
`eb59823a`, and this re-exports the closure there with
`tools/sync-native.py`.

Main reaches the closure through the seat clipboard and Cmd chords
(kore-abi, kore-window), Files' DagFS watch plane and kore-rt's owed
service wakes, DagFS's cwd anchor and path folding, and #1494's reseal
digests in five services' `boot.toml`. The closure is 27 packages and 406
files. Against Kore `e6b108d1`, only kore-rt's `futex.rs` and
`thread_controller.rs` differ. `native/Cargo.lock` is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACHC7hXQ3xRtyu6Yt31jXQ
`ft/fervent-shannon-egp5pt` moved to `2ce41f1`: a C program's `main`
receives its arguments (`crates/c`), and `native/` is re-exported at Kore
`dfece644`, where the process page carries the name a program was run by.

Both sides changed `native/`, which is never merged by hand.
`tools/sync-native.py` re-exports it at Kore `5c09f155`, the merge of
#1494's round into MicroPerceptron/kore#1534's. The closure is 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 from #1494's later reseals differ too.
`docs/provenance.md` records the export. Everything outside `native/`
merges cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACHC7hXQ3xRtyu6Yt31jXQ
Kore's #1494 moved to `fe9603e3`: the kernel offers a function to
drivers only on positive evidence, and a claim names its holder.
MicroPerceptron/kore#1534 merges that at `7c0be6e8`, and this re-exports
the closure there with `tools/sync-native.py`. libkore#32's branch has
not moved, so nothing else changes.

kore-abi's device state changes and gains its device classes
(`device/class.rs`). The closure is 27 packages and 408 files. Against
Kore `fe9603e3`, only kore-rt's `futex.rs` and `thread_controller.rs`
differ, and `services/swap/boot.toml`, whose digests are this change's
reseal over the ioctl round. `native/Cargo.lock` is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACHC7hXQ3xRtyu6Yt31jXQ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants