Skip to content

fix(client): accept a secret that arrived before the read deadline - #27

Merged
novelKR merged 1 commit into
mainfrom
claude/project-thread-af6ntg
Oct 7, 2026
Merged

novelKR merged 1 commit into
mainfrom
claude/project-thread-af6ntg

Conversation

@novelKR

@novelKR novelKR commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Requested by Seongjae · project thread

Fixes #24. It is an independent product fix outside CS-RG, made before CS-RG U4 as #24 asks, the same way novelKR/CodeSpace#87 fixed novelKR/CodeSpace#85 before U3. CodeSpace picks it up only through a separate pin change that needs the owner's approval.

Why

Before: read_owned fixed a 250 ms deadline and, on every pass, failed as soon as the deadline had passed, before it looked at the descriptor again. A receiver that was not scheduled for 250 ms therefore failed with invalid, missing or expired private credential FD and closed its one-shot descriptor, even when the whole secret and its EOF had been written in time. CodeSpace's macOS CI saw this in 4 of 143 gateway runs and in about one in ten registration stages (#24).

After: the 250 ms deadline bounds only waiting for bytes that have not arrived. Once it has passed, the receiver still takes bytes and an EOF that are already readable, and it fails on the first pass that finds nothing. A writer that never writes or never closes still fails at the deadline, as before.

This matters most for the launch helper. devguard-launch re-executes itself at utility QoS and only then reads its one-time permit (crates/launch/src/main.rs:172-195), after BeginLaunch has committed the launch, so a lost permit read leaves the owner to release the attempt with AbandonLaunch { HelperExited }. CS-RG U4 is the first CodeSpace unit to run the helper. The other take_inherited callers (the CodeSpace UDS worker's consumer secret, and the lease tokens of devguard exec and devguardd candidate) gain the same tolerance.

Changes

File Change
crates/client/src/credential.rs read_owned delegates to a private read_owned_by(fd, deadline). Past the deadline, poll uses a zero timeout, and an EINTR from poll or a read that returns WouldBlock or Interrupted fails instead of looping, so each further pass takes at least one byte or the EOF and the loop stays bounded. Before the deadline the loop is unchanged. Two unit tests stand an already-passed deadline in for the stall: a secret written and closed in time is accepted, and an empty, half-written or EOF-less secret still fails with Unauthorized
docs/contracts.md, docs/ko/contracts.md The CredentialHandoff paragraph states the new bound
docs/translations.json Korean counterpart reviewed; the contracts hashes recorded with scripts/check_docs.py record

What does not change

  • The public API, the 64-byte secret, the 65-byte read bound, the error returned, and descriptor handling: close-on-exec and nonblocking setup, and consuming and closing the descriptor on success and every error.
  • The 250 ms frame deadline (docs/contracts.md:19) and the helper's 250 ms authorization deadline (docs/contracts.md:95). A reply that misses them is still a refusal, and the helper still never execs on one.
  • No wire, journal, configuration, dependency, CI policy or test-retry change. CodeSpace keeps its DevGuard pin f1f9084 and its session retries until a separate change.
  • The qualified DG-1 release 0.1.0-5daee5d-b3fa569e is not rebuilt. An installed helper gets the fix only from a new release, which is a separate decision.

Verification

At 3ae5dc2, based on main at 035936d, in a Linux x86_64 container with Rust 1.95.0:

  • Reproduction. With main's deadline loop restored under this head's read_owned_by split, a_secret_written_and_closed_in_time_is_read_after_the_deadline fails, which reproduces Credential reader rejects an already-buffered secret when its thread stalls for 250 ms #24 deterministically. With the fix it passes. The existing a_full_secret_without_writer_eof_expires_and_closes_the_fd passes unchanged.
  • python3 scripts/ci_plan.py --base origin/main --head HEAD: profile affected, component client (3 normative paths, 1 component path), Rust on both platforms, suites dg1-authority and dg1-auth on ubuntu-24.04 and eleven DG-1 suites on macos-14.
  • python3 scripts/validate.py --stages whitespace,source-contract,documentation,protocol-analysis,dependency-boundary,format,clippy,contracts --diff-base origin/main (run 20261007T153623Z-8e62f9c8): every stage passed; 200 tests passed, 0 failed, 3 ignored. report.json SHA-256 7b9a23368d0b7a5b20c84c978d9e7770920dacbc1780cf1b71a6227090e1edd4.
  • The validator report and logs, the plan and the reproduction log are preserved outside the repository with a SHA256SUMS file (SHA-256 4df7a04477ab0fca696611179c63ee56a736079c03e6c3da8b7075e5c3526c9a).

Hosted CI, run 37645735472 attempt 1, on merge source 1318bca (this head into 035936d): Plan, Repository checks, contracts (ubuntu-24.04), contracts (macos-14) and Required checks all succeeded. The complete validator passed on both platforms, and so did every planned suite, except that dg1-launch and dg1-cargo are incomplete only for the cases scripts/ci-policy.json allows on hosted macOS runners: claimed-then-refused, which needs an unclamped helper, and the Cargo cases the runner's capacity cannot fit. Required checks matched each against its allowance. Artifact SHA-256 digests, retained until 2027-01-05:

  • dg0-macos-14-1: ac1a053c250a145e3acdfa83c0d9596f05ca03199dc84da4db43d16315c52612
  • dg0-ubuntu-24.04-1: 02f07dd029054eba1774b12857422530672e3ba1ced5c29781595c6dea42a5b2
  • ci-plan-1: ce9aae06150097c03f4b2ccbd24532059b2a4ea803b21652d5ee2617cafedf45
  • ci-repository-1: 37eff04ab2a2ea2662990c6919c95e2e94571dbaef19c66ec195cb7bb4fd5668

Rollback

Revert the merge (git revert -m 1 <merge commit>). The reader then fails at the deadline again even when the secret is already buffered. Nothing persisted depends on this change.

read_owned failed as soon as its 250 ms deadline had passed, before it
looked at the descriptor again. A receiver that was not scheduled for
250 ms after fixing the deadline therefore failed and closed the
descriptor even when the secret and its EOF were already buffered (#24).

The deadline now bounds only waiting for bytes that have not arrived.
Once it has passed, poll does not wait and a pass that makes no progress
fails, so the receiver takes only bytes and an EOF that are already
readable. A writer that never writes or never closes still fails.

read_owned_by takes the deadline, so the tests stand an already-passed
deadline in for the stall on both platforms. The contract describes the
new bound in English and Korean; the contracts hashes are recorded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants