Skip to content

daemon: Harden request cancellation and transport ownership - #225

Draft
Preovaleo wants to merge 8 commits into
linux-credentials:mainfrom
Preovaleo:perso/generic_transport-split/2
Draft

daemon: Harden request cancellation and transport ownership#225
Preovaleo wants to merge 8 commits into
linux-credentials:mainfrom
Preovaleo:perso/generic_transport-split/2

Conversation

@Preovaleo

@Preovaleo Preovaleo commented Sep 5, 2026

Copy link
Copy Markdown

Draft — depends on #224. Review the isolated changes in the fork PR.

Split and stacked review

Original PR: linux-credentials/credentialsd#220.

The replacement is split into four parts. Parts 1 → 2 → 3 form native GitHub stack #4 on the fork; part 4 is independent.

Part Upstream integration PR (targets main) Focused stacked review on the fork
1 — Shared transport lifecycle #224 Preovaleo/credentialsd#2/1main
2 — Cancellation and request ownership #225 Preovaleo/credentialsd#1/2/1
3 — Silent transport exhaustion #226 Preovaleo/credentialsd#3/3/2
4 — Discovery concurrency bound #227 Independent; review directly upstream

Review and integrate parts 1 → 2 → 3 in order. Part 4 can be reviewed and merged independently.

GitHub does not support native stacks across forks. Use Files changed on the fork PRs for incremental review of each layer. The upstream PRs target main; #225 and #226 include earlier layers until their dependencies land and the branches are rebased. Integration into the main project happens through the upstream PRs; merging the fork stack only updates the fork.

Hello 👋,

Summary

This PR extracts the cancellation and request ownership fixes from #220. It builds on the shared transport lifecycle introduced in part 1.

Two lifecycle problems remained:

  • a stream waiting for its next transport event could remain pending after cancellation;
  • a late terminal event could attempt to complete whichever request context was currently active, including a newer request.

Discovery also let individual transports read the active request separately, allowing ownership and cancellation data to come from different snapshots.

This PR:

  • makes pending transport streams wake when their request is cancelled;
  • binds terminal events to their originating request ID and unique request marker;
  • checks ownership before consuming the active request context;
  • captures one request lifecycle snapshot and passes it to every selected transport;
  • adds coverage for cancellation and terminal events becoming ready in the same poll;
  • updates the architecture documentation and changelog.

Result

Cancellation no longer depends on additional transport activity.

A terminal event can complete a request only if its ownership still matches the active context. An event from an earlier request cannot consume the response channel or complete a replacement request.

Every transport selected by a discovery attempt receives the same request snapshot, including its identity and cancellation token.

The post-poll cancellation check also discards a terminal event when polling the underlying stream makes that event ready while cancelling its request.

Scope and dependencies

This PR depends on part 1:

perso/generic_transport-split/1

Review the changes relative to that branch.

Failure after global transport exhaustion is introduced in part 3. The D-Bus discovery concurrency bound is handled independently in part 4.

Diff composition

Scope Test code Daemon code Moved/reused text (included)
This layer, relative to part 1 ≈52% ≈48% ≈7%
9655d8d — Wake cancelled streams ≈80% ≈20% ≈0%
214a719 — Bind results to their request ≈53% ≈47% ≈0%
c04af6b — Share one lifecycle snapshot ≈25% ≈75% ≈15%
d785725 — Test cancellation during polling ≈100% ≈0% ≈0%

Commit sequence and rationale

The commit order follows the dependencies between these changes:

  1. daemon: Wake credential streams when requests are cancelled

    The common stream still waits on transport activity before noticing cancellation. Making that wait cancellation-aware addresses the case where the transport remains idle indefinitely. This comes first because the stream must be able to stop independently of its backend; it does not yet establish whether a result belongs to the active request.

  2. daemon: Bind terminal events to their originating request

    Cancellation and request ownership answer different questions. A late result must not consume whichever context happens to be active when it arrives. The stream therefore carries its originating request ID and a unique marker, and complete_request checks ownership while taking the context under the same lock. The marker ties completion to a particular request instance, including when numeric IDs coincide. This makes acceptance of a terminal result depend on the context it owns, rather than on an earlier observation of cancellation.

  3. daemon: Carry one lifecycle snapshot across transports

    Once ownership is explicit, discovery must distribute it consistently. Separate context reads for each transport could straddle a request replacement and create one discovery attempt from different requests. Capturing the request and RequestLifecycle together, once, gives every selected transport the same request, identity, and cancellation token. This follows the ownership commit because it carries that identity through discovery setup; it also provides the snapshot used by aggregate completion in part 3.

  4. daemon: Test simultaneous terminal cancellation

    There is a remaining boundary in the polling contract: the underlying stream can cancel the token and return a terminal event in the same poll. The test makes that happen inside the poll itself, so it exercises the post-poll cancellation check without relying on task scheduling. It adds coverage for a guard already present in the common stream and explains why the cancellation-aware wait does not make that guard redundant.

  5. docs: Describe request ownership and prompt cancellation

    The documentation brings the two guarantees together: cancellation wakes pending streams, and completion is scoped to the originating request. Recording them separately matters because prompt cancellation alone would not describe the protection against stale results.

Testing

Validation performed at the branch tip:

  • cargo test -p credentialsd --bin credentialsd — 33 tests passed
  • cargo clippy -p credentialsd --bin credentialsd -- -Dwarnings — passed
  • cargo fmt --all --check — passed
  • git diff --check — passed

The suite includes the transport coverage from part 1 and regressions for pending-stream cancellation, stale terminal events, and terminal events becoming ready during cancellation.

Théo Bougé added 8 commits September 5, 2026 15:52
The push-based helper was embedded in the credential service tests, and its cancellation-focused names hid that it could script any transport state. Move it to test_support, rename it around that broader role, and share typed completion and failure helpers across USB, hybrid, and NFC tests.
USB, hybrid, and NFC duplicated state conversion and request completion in separate stream wrappers, making their terminal behavior easy to desynchronize. Route typed transport events through one lifecycle stream and cover success and failure for every backend.
A lifecycle stream only noticed cancellation after another transport event, so idle discovery could remain pending. Await transport polling through CancellationToken::run_until_cancelled so cancellation wakes the stream immediately.
A late terminal event could consume whichever request context was active and complete a newer request. Attach the request ID and a unique marker to each lifecycle stream, then ignore terminal results whose ownership no longer matches.
Discovery let each transport read the active request independently, so a replacement between reads could mix ownership and cancellation data. Snapshot the request ID, marker, and token once and pass that typed lifecycle to every selected transport.
A terminal event can become ready in the same poll that cancels its request. Without the post-poll cancellation check, that event could still consume the active context.

Cancel the token from inside the scripted poll and verify that the terminal result is discarded without sending a response.
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.

1 participant