Skip to content

toyos-crypto: P-256 on register kernels, and a thread's own timing protection (PSTATE.DIT) around secret work - #1063

Merged
Japabu merged 7 commits into
mainfrom
p256-speed
Nov 3, 2026
Merged

Japabu merged 7 commits into
mainfrom
p256-speed

Conversation

@Japabu

@Japabu Japabu commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Draft, ready for review. This is #1063 and #1038 combined into one change, rebased onto current main (head dae2e3f). It supersedes #1038, which is closed and keeps its branch. Metal is staged at 9a5d4fa but has not run yet. Review r1's fixes are 2b680cc and review r2's are b6fc0c6; below, "the boundary".

Problem

  • P-256 is slower than ring. On every TLS handshake, toyos-crypto's P-256 ECDH was 1.39–1.47× ring and ECDSA-P256 verify 1.27–1.38×. The algorithms already matched AWS-LC's, so the gap was overhead in each operation:
    • one CPUID-cache branch per field operation;
    • operands passed through memory;
    • carries turned into flags.
  • No thread could ask for its CPU's data-independent timing mode. The owner approved a scoped per-thread API (item 67): turn the mode on, get the previous state back, restore it after the secret work, with toyos-crypto using it automatically.

Change

P-256 dispatches once per computation, on register kernels (commit 1)

This is #1063's code unchanged: the patch-id of origin/main..98d3bebf3 equals that of #1063's 2a5b446b0..9dc8a7771. Its three rounds are squashed into one commit, so the swap that round 3 undid is not in the history.

  • The modulus type carries the arithmetic. Modulus holds add, double, sub, mul, square and select. Modulus::dispatch runs a Computation once:
    • P256Field sends it to arch::p256, which checks CPUID once.
    • Where BMI2/ADX are admitted it runs on P256Kernels. That type is private to arch/x86_64.rs.
  • Register-operand kernels.
    • Multiplication keeps a and b0 in registers and forms q·p3 by shifts.
    • Squaring reduces by mulx.
    • Final subtractions use cmov or borrow masks.
    • The 16-entry table reads use AVX2 vpand/vpandn/vpor under an opaque mask.
  • Fewer steps.
    • The variable-base table is built by doubling.
    • Half-delta divsteps take 10×59 steps for p ≤ 256 bits (Wuille's safegcd document).
    • Verification inverts its public s in variable time.
  • P5, the w = 7 base table, stays declined under item 78.

A thread's own timing protection, scoped, with toyos-crypto under it (commit 2)

  • The interface. toyos::timing::protect() turns the mode on for the calling thread where the thread can set one. It answers a !Send Protection that puts back the state it found:

    • protections nest;
    • an early return or an unwinding panic restores the state;
    • no right is needed.
  • AArch64: PSTATE.DIT. EL0 writes it. Every exception from EL0 saves it in SPSR_EL1 in the thread's trap frame, and the return restores it, so it follows the thread across switches and CPUs with no switch work.

    • SYS_TIMING_CONTROL (165, above main's SYS_DEVICE_FIRMWARE_WINDOW) only tells a program that every CPU has FEAT_DIT, which EL0 cannot read itself.
    • control_regs::check holds every CPU's FEAT_DIT to the boot CPU's.
    • The SDK then uses MRS/MSR directly. The write is not nomem, so the compiler cannot move the protected work's memory accesses out of the protected span.
  • x86-64: no mode. SYS_TIMING_CONTROL answers false and every Protection is inactive. Nothing reports x86-64 as protected.

  • The boundary: the operation that is secret by construction. In toyos-crypto, every operation that computes on a key or other secret takes the protection on its first line. Constructors that only store a key take none.

    • ciphers, MACs and KDFs;
    • keyed SipHash and ct::equal, added in review r1;
    • every secret-key and agreement operation.

    The sites were re-derived against main's current API. A plain hash serves public data as often as secret and takes none, and neither does verification.

    • The guard is exported as toyos_crypto::timing::protect for a caller that hashes a secret.
    • toyos-ssh's exchange hash over K and its RFC 4253 §7.2 key derivation take it.
    • put_mpint encodes K for that hash. It used to find K's first nonzero byte by branching on each byte. Since b6fc0c6 it reads every byte and counts arithmetically. The encoding's length still shows K's leading-zero count, as RFC 4251's mpint (prescribed for K by RFC 8731) publishes it.
    • No other non-test caller in the tree hashes a secret. I searched every hash:: user outside the crate; bcachefs's and sshserver's are test-only.
    • netstack's keyed SipHash and wlan's MIC and key comparisons are covered through the primitives themselves.
  • Records.

    • issues/intels-store-forwarding-predictor-times-constant-time-code-by-its-data.md records the T14 finding, with evidence and an exit.
    • issues/the-avx2-poly1305-runs-under-intels-mxcsr-dependent-timing.md records Intel's MXCSR-dependent timing (MCDT) on the AVX2 Poly1305's VPMULUDQ. The T14 does not report itself immune.
    • The crypto track gains the protection and cites both.

How the two relate

  • A P-256 key generation, public key or agreement is one computation that Modulus::dispatch runs. The protection is taken at the public entry, so the whole computation runs inside it.
  • On AArch64 that span is under PSTATE.DIT.
  • On x86-64 the guard is a constant inactive value: the kernels' speed is untouched, and their constant time rests on their code. crypto_secret_timing_curves on both benches checks that.

Decisions to review

Checks (evidence: https://github.com/ToyOSOrg/evidence/tree/f5943573d942d8f78edb4f6d9420592c12119f43/combined/r1)

check revision result
toyos-crypto lib, differential, Wycheproof, vectors, NIST and fuzz suites, x86_64-linux-musl under QEMU TCG -cpu max (BMI2/ADX/AVX2: the kernels), including p256_computations_run_on_the_kernels_the_cpu_admits 9a5d4fa's tree each exit 0
cargo test --test toyos-build -- virt_smp virt_el1_smp (AArch64, 8 emulated CPUs, FEAT_DIT) 9a5d4fa exit 0. "PSTATE.DIT followed 16 threads across 1914 / 1610 switches between states, and 8 protected threads ran on more than one CPU"
control: the kernel clears SPSR.DIT on every EL0 interrupt return 9a5d4fa + patch exit 1: "thread 0, which asked for true, read another state in round 2"
control: P-256 SecretKey::generate takes no protection 9a5d4fa + patch exit 1: "toyos-crypto drew its secret unprotected"
cargo run -- --build-only (x86-64) and --arch aarch64 9a5d4fa exit 0 each
cargo run -- --ci host 9a5d4fa exit 0, 108 steps green
review r1 fixes: --build-only x86-64 and aarch64; virt_smp virt_el1_smp (2021 / 1578 switches between states, 8 migrated); cargo run -- --ci host 2b680cc each exit 0; 108 steps green (r2 evidence)
review r2: cargo test -p toyos-ssh (with the new mpint cases), toyos-crypto lib tests, clippy -p toyos-crypto -p toyos-ssh (all features and default), -D warnings b6fc0c6 each exit 0. Control: the sign rule without its first-nonzero condition makes the mpint test exit 101
  • Why a guest. The AArch64 claim is about the kernel's exception path: whether SPSR_EL1.DIT survives every switch and migration. No host test reaches that. The job runs only on AArch64 (RUST_SKIP on x86-64, which has no such state).
  • Supported targets. QEMU AArch64 TCG max, in virt_smp and virt_el1_smp. No ARM bench exists. AArch64 without FEAT_DIT (cortex-a72) takes the same one-line control_regs answer and is not run.
  • What r1 changed for the earlier evidence.
    • The QEMU x86 crypto suites and the two controls ran at 9a5d4fa.
    • r1 adds the guard to ct::equal and siphash24. Off ToyOS the guard is nothing, so the host and Linux suites run the same code. On x86 ToyOS it is a constant None.
    • r1 refactors the SDK guard to a Mode token. virt_smp's nesting, early-exit and panic asserts held at 2b680cc.
    • The staged metal images (9a5d4fa) still measure the head's x86 P-256 and constant-time code.
  • Reused P-256 evidence. The code is byte-identical by patch-id, so toyos-crypto: P-256 on register kernels, and a thread's own timing protection (PSTATE.DIT) around secret work #1063's mutants (20 across three rounds, all red) and its Linux-probe bench rounds still apply to it. Main's intervening crypto changes (ChaCha stitched AVX2, ML-KEM, scrypt) are covered by the suites above, run at this tree.

Metal (run by the orchestrator; evidence https://github.com/ToyOSOrg/evidence/tree/aa3369406af00b46a4b76476ef4cea7df040e30f/combined/r3)

The images were staged at 9a5d4fa, with main 190f3b1 as the A/B base. Every boot exited 0. Every judge exited 0 at the rebased head: crypto_instrument on all eight cryptocase readbacks, and crypto_secret_timing_curves on both benches.

Constant time: PASS on both benches. 1,000,000 measurements a routine, no CPU timing mode:

P-256 public key P-256 agreement P-384 public key P-384 agreement Ed25519 null leaky
T14 3.20 1.51 1.19 1.42 2.73 1.22 / 1.70 855.8 / 890.6
AMD 2.99 1.49 2.47 2.43 1.66 1.14 / 2.12 140.1 / 169.3

Values are max |t|; the bound is 4.5.

Speed A/B: the predeclared rule fails on two of 312 cells. It reads ours, head over base, both pairs:

bench, build p-256/ecdh ecdsa-p-256 verify
T14 asserted 0.817, 0.818 0.848, 0.839
T14 unasserted 0.804, 0.889 0.826, 0.912
AMD asserted 0.793, 0.792 0.760, 0.779
AMD unasserted 0.752, 0.752 0.774, 0.763

P-384 is 0.95–0.99 everywhere except T14 unasserted pair 2.

  • T14 unasserted, pair 2. Every row of that run, ring's included, was 1.07–1.11× slower than base-2. Ring's code is identical in both images, so this was the boot's drift. Divided by ring's ratio in the same boots, verify is 0.822 and 0.826 and ECDH 0.806 and 0.805, in line with the other three runs. Read raw, verify is 0.912 > 0.90 and fails the rule as written.
  • AMD asserted chacha20-poly1305/key: 5.3 → 6.0 and 6.9 ns (1.13, 1.30), ring unchanged.
    • Nothing this PR changes runs in that row. SealingKey::chacha20_poly1305 takes no guard, and its constructor and Secret wipe are unchanged.
    • The two head images of the same commit read 6.0 and 6.9. The unasserted build and the T14 show nothing: 1.00–1.04.
    • My inference, not verified by disassembly: this is code layout from the crate's growth in a ~5 ns row. It still breaks the rule's "nothing else above 1.05 in both pairs".
  • Every other row passes, including both P-256 rows in every other cell, all ≤ 0.89 raw.
  • Ours against ring on the head (head-1): T14 asserted ECDH 1.18× ring, verify 1.10×; unasserted 1.16× and 1.07×. AMD asserted ECDH 1.08×, verify 0.97×; unasserted 1.02× and 0.97×.

Settling the two cells (orchestrator's ruling: evidence, not normalisation)

  • Disassembly (r4).
    • chacha20-poly1305/key's loop, constructor and drop have identical instructions in base and head.
    • Only SealingKey::chacha20_poly1305 moved: 0xc9180 (64-byte aligned) to 0xf5360 (+32).
    • The builds are deterministic per source.
  • Pair 3 (r5): head 41977c5 against base 8ca3a2e, with the rule restated in the request before the run.
    • T14 unasserted verify: 0.840. It passes, as do every P-256 cell (0.75–0.84) and every other cell. The T14's highest other cell is 1.036.
    • AMD asserted chacha20-poly1305/key repeats: 5.3 → 6.0 ns (1.132), ring unchanged.
    • Per the orchestrator's ruling, it is recorded in the crypto track as a measured layout effect (c294387).

Unresolved risks

  • The T14 stays above ring on P-256: 1.19× on ECDH and 1.10× on verify (Linux probe, toyos-crypto: P-256 on register kernels, and a thread's own timing protection (PSTATE.DIT) around secret work #1063 r3).
  • FSFP is not mitigated: on the T14, constant time rests on avoiding the value pattern that once leaked. A chosen-value test cannot cover every value (the new issue).
  • MCDT is not mitigated: the AVX2 Poly1305's VPMULUDQ runs under the default MXCSR (the new issue).
  • P256Kernels soundness rests on type privacy and the dispatch check, not a compile-time token.
  • Host builds of toyos-crypto take no protection: macOS and Linux, including AArch64 with DIT. Item 67 covers ToyOS.

🤖 Generated with Claude Code

Japabu added a commit that referenced this pull request Nov 1, 2026
ECDH and ECDSA verification ran every P-256 field multiplication through
a CPU feature check, with both operands passed through memory, and added
and subtracted in Rust whose carries compiled to flag materialisation.
Now the arithmetic is a property of the modulus type:

- `Modulus` carries the runtime operations (add, double, sub, mul,
  square, select), generic by default; `P256Field` overrides them with
  its word by word forms. `Residue` calls them, with no per-operation
  branch.
- `Modulus::dispatch` runs a `Computation` (a visitor generic over the
  modulus type) on the fastest arithmetic for that p. `P256Field` sends
  it to `arch::p256`, which checks CPUID once and runs it on
  `P256Kernels` where BMI2 and ADX are admitted. That type is private to
  arch/x86_64.rs, so nothing names the kernels without the check.
- The kernels take and return their words in registers. Multiplication
  holds a and b0 (in RDX) in registers and reads one word of b per row
  from row 1, forming q p3 by shifts, as a template has 13 general
  registers plus RDX and none is left for p3. Holding b instead and
  reading a from memory was faster in an isolated chain but slower in
  the AMD bench's formulas, where the memory operand was just stored:
  store-to-load forwarding sits on the critical path, slower on Zen 2
  than on Tiger Lake. Squaring reduces by mulx with p3 in a1's register
  once a1 is squared. Final subtractions are `cmov` or a mask from the
  borrow.
- The 16-entry masked table reads, of the variable-base table and of the
  comb's rows, go through `Modulus::select`: portable `ct` masks by
  default, and on AVX2 one vector per residue, kept or replaced under a
  vector equality of the index with a count of the entries. The mask is
  opaque to LLVM, so the select stays vpand/vpandn/vpor and does not
  become vblendvpd. The loads are `lddqu`: `_mm256_loadu_si256` copies
  through the stack under debug assertions.
- `Point` and `Affine` take the modulus type as a defaulted parameter, so
  agreement, public-key derivation and verification are each one
  dispatched computation; static tables stay in `P256Field`'s type and
  cast per read.

Also, from the same analysis:

- The variable-base table takes each even multiple by doubling and each
  odd one by one addition: 8 doublings and 7 additions, not 1 and 14.
- Inversion modulo a p of at most 256 bits takes divsteps with delta
  starting at 1/2: 590 steps suffice there (Wuille, libsecp256k1's
  safegcd_implementation.md), 10 batches of 59 against 12 of 62. The
  batch matrix is scaled to 2^62 whatever its step count, so applying it
  is unchanged. P-384 keeps delta = 1 and the paper's bound.
- Verification inverts its public s in variable time: runs of zeros at
  once, six bits cleared per multiple of f, stopping when g is zero.

On the Linux probe against main 2a5b446 in the same runs, ECDH went
from 78.6 to 63.1 us on the T14 and 70.8 to 53.1 on the AMD bench, and
ECDSA-P256 verification from 65.2 to 54.8 and 60.0 to 47.0 (#1063's
evidence).

Tests: each batch variant against the divstep definition; the public
inverse against integers; the point checks in the dispatched arithmetic;
the dispatch's choice against CPUID; and the P-256 differential,
Wycheproof, CAVP and DER tests now run in the dispatched form too, which
is no longer the portable code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu Japabu changed the title toyos-crypto: P-256 dispatches once per computation, on register kernels toyos-crypto: P-256 on register kernels, and a thread's own timing protection (PSTATE.DIT) around secret work Nov 1, 2026
@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1063 at fcd1e37 (P-256 kernels + per-thread timing protection)

Reviewed against main 190f3b1, the merge base. main has moved two commits since then. Neither touches these files, and git merge-tree reports a clean merge. The P-256 commit 98d3beb has the same git patch-id --stable as 2a5b446b0..9dc8a7771 (ed7b7732…). fcd1e37 changes only issue records over 9a5d4fa, so the evidence gathered at 9a5d4fa applies to this head. No earlier review exists on #1063 or #1038: their only comment is #1038's closing pointer. So I also checked the P-256 commit on the points in the brief rather than treating it as already reviewed. The DOITM ruling is not reopened here.

Architecture

The split of responsibilities is right, and it is small:

  • The thread owns PSTATE.DIT itself.
  • The kernel's existing single vector path saves and restores SPSR_EL1 in the frame on the thread's own kernel stack, so the bit follows the thread across switches and migrations with no switch work.
  • The kernel adds only what EL0 cannot read: FEAT_DIT on every CPU.

I checked the kernel side:

  • trap.rs saves SPSR_EL1 at entry and reloads it from the frame before eret.
  • No kernel code writes frame.spsr.
  • process_start and thread_start start every thread at SPSR_EL1 = 0, so a new thread starts unprotected.
  • hold_dit decodes bits 51:48 correctly. An asymmetric machine fails loudly, which is correct for the trusted CPU.
  • The syscall takes no arguments and answers a bool, so it has nothing to validate.
  • Generic dispatch.rs names crate::arch::control_regs::…, as other generic code already does.

The guard's semantics are sound:

  • The previous-state model nests.
  • Drop covers an early return and an unwinding panic.
  • PhantomData<*const ()> makes the guard !Send and !Sync.
  • Every toyos-crypto site binds let _protection, never let _.

On x86-64 the guard is a constant inactive value, as ruled.

P-256 on x86 (arch/x86_64/p256.rs), as far as code inspection establishes:

  • No branch or address depends on the operands.
  • Reductions use cmov or sbb masks.
  • Only mulx, adcx, adox, add/adc and shifts are used.
  • select reads all 16 entries with vpcmpeqq/vpand/vpandn/vpor under an opaque mask.
  • inverse_of_public is reached only from verification's public s.
  • Residue::cast holds p equal at compile time.

P256Kernels soundness rests on module privacy plus the single CPUID-gated call site. That is acceptable and recorded as a risk.

The Intel guidance confirms the MCDT record:

  • VPMULUDQ is on Intel's list.
  • MCDT_NO is CPUID.(7,2):EDX[5].
  • The delay is at most one cycle.
  • The STMXCSR/LDMXCSR 0x1FBF/LFENCE sequence matches.

No other MCDT-listed instruction occurs in lib/crypto/src/arch/x86_64: only _mm256_mul_epu32 in poly1305.rs:51.

Size: +1592/−476 lines. The production lines are dominated by the P-256 kernels. The timing commit's production code is about 110 lines across the SDK, ABI and kernel. The guest test is about 225 lines with its helper.

Findings

BLOCKER 1 — the protection's coverage boundary leaves real secret work unprotected, and the contract's caller duty is one no caller meets.

  • Where: lib/crypto/src/timing.rs:4-6, and the "every public secret operation" claim in 9a5d4fa's message and the PR body.

  • Problem: hashes, SipHash and ct take no protection, and the doc says "a caller holds one around SipHash under a secret key". Neither external caller does, and a portable library cannot conveniently do it: timing is a private module of toyos-crypto, so a portable crate reaches the guard only through its own cfg(target_os = "toyos") dependency on the SDK. Concrete unprotected secret work on an AArch64 machine with FEAT_DIT:

    • lib/ssh/src/kex.rs:135-171: the exchange hash and RFC 4253 §7.2 key derivation run SHA-256 over the shared secret K and derive every session key. hybrid::for_ssh hashes the same secrets protected, which is inconsistent.
    • system/netstack/ip/src/draw.rs:41,63-64 and limiter.rs:59,67: SipHash under the stack's secret keys.
    • system/wlan/handshake/src/eapol.rs:122 and supplicant.rs:474,487: ct::equal on a computed MIC and on keys.
  • Failure scenario: sshd on a FEAT_DIT machine derives its session keys with PSTATE.DIT clear, although the PR states that secret work runs under the thread's protection.

  • Fix (either):

    • (a) Take the protection inside ct::equal, siphash24 and the hash entry points. That costs one Relaxed load and an MRS per call on AArch64, and nothing on x86. If public bulk hashing under DIT is a concern, measure it.
    • (b) Export the guard from toyos-crypto (toyos_crypto::timing::protect) and take it at those callers.

    Either way, make the module doc, commit message and PR body state the actual boundary, and delete the unmet caller duty.

BLOCKER 2 — the constant-time evidence for the new x86 kernels is owed.

  • What is missing: metal cryptoctcurvescase (crypto_secret_timing_curves) on the T14 and the AMD, and the predeclared cryptocase A/B. Both are staged and have not run.
  • Why it blocks: the claim that the new mul/square/select kernels, including the AVX2 table read, are constant time on x86 rests on this check. With no x86 timing mode, the code alone carries that claim. The PR says this is owed; it is listed here so that landing waits for it.
  • To resolve: run the orchestrator's boots, add the stage-4 P-256 verdicts to the crypto track, and fill in the PR.

SUGGESTION — the nomem comment promises less than a reader may assume.

  • Where: sdk/toyos/src/arch/aarch64.rs:120.
  • Issue: omitting nomem orders memory accesses only. Register-only work, such as the pure, nomem P-256 kernel asm or ct reductions, can in principle be scheduled across the MSR. In practice operands are loaded after protect() and results are stored before the drop, so the risk is small.
  • Suggestion: either state that limit in one clause, or accept it knowingly. Apple's and other DIT wrappers have the same limit.

SUGGESTION — the DIT mechanism is described in four places.

  • Where: kernel/src/arch/aarch64/control_regs.rs:104-108, sdk/abi/src/syscall.rs:2618-2625 and sdk/toyos/src/timing.rs, plus the commit message.
  • Suggestion: keep the save/restore invariant at the kernel function and reduce the ABI doc to "every CPU has FEAT_DIT; see toyos::timing".

SUGGESTION — make the x86 set_mode unrepresentable.

  • Where: sdk/toyos/src/arch/x86_64.rs:48, unreachable!.
  • Suggestion: have mode() return a token that set_mode requires, so x86 has no set_mode at all.

SUGGESTION — the FSFP record's PSFD sentence is ambiguous.

  • Where: issues/intels-store-forwarding-predictor-times-constant-time-code-by-its-data.md:23.
  • Issue: "later parts enumerate PSFD" reads as if Tiger Lake does not. The T14 reads CPUID.(7,2):EDX = 0x1, so it enumerates PSFD, and the record itself turned PSFD on there.
  • Suggestion: say the T14 enumerates PSFD.

SUGGESTION — the new cfg(target_os = "toyos") arm is not linted.

  • Where: lib/crypto/src/timing.rs:9-10.
  • Issue: no build/clippy.rs shape lints a ToyOS target, so this arm is never linted. That matches existing practice for every ToyOS-only arm, and this one is a single use.
  • Suggestion: record it if guest-target clippy coverage is wanted.

Tests and evidence

Checked in the implementer's logs:

check result
virt_smp / virt_el1_smp exit 0, with the judged line (1914 and 1610 state changes, 8 migrated)
control: SPSR.DIT cleared on EL0 IRQ return exit 1, "read another state"
control: unguarded generate exit 1, "drew its secret unprotected"
x86-64 suites under QEMU -cpu max exitcode=0, with the dispatch test and the AVX2 select test passing
--ci host 108 steps green at 9a5d4fa (ci-head.txt)

The guest test is focused and justified, its premises are checked in the trace diary, and its targets are declared through RUST_SKIP and virtsmpcase. The IRQ control shows the test reads state after a real eret. SVC and context switches go through the same save/restore, so one control covers that path. Whether every crypto entry holds the guard is established by inspection, which is adequate, apart from the boundary in Blocker 1.

The NIST log ends after 4 of its 5 tests with exitcode=0 and no test result line, presumably output lost at the guest's exit. The exit code stands.

SEND BACK

Japabu added a commit that referenced this pull request Nov 1, 2026
…ruction

The boundary is the operation that takes a secret. Keyed SipHash and
`ct::equal` now take the thread's protection themselves, as the ciphers,
MACs, KDFs and key operations already did; netstack's keyed SipHash and
wlan's MIC and key comparisons are covered through them. A plain hash
serves public data as often as secret and takes none, so the guard is
exported as `toyos_crypto::timing::protect` for a caller hashing a
secret: `toyos-ssh`'s exchange hash over K and its RFC 4253 §7.2 key
derivation take it. No other caller outside the crate hashes a secret
outside a test. The crate's module doc and the crypto track state that
boundary, and the caller duty no caller met is gone.

Also, from the review's suggestions:
- x86-64's timing mode is an uninhabited `Mode`, so nothing there can
  set one; AArch64's `Mode` exists only once FEAT_DIT is known.
- The DIT write says that only memory accesses are ordered by it.
- `SYS_TIMING_CONTROL`'s doc points at `toyos::timing`; the save and
  restore invariant stays at the kernel's `user_timing_control`.
- The FSFP record says the T14 enumerates PSFD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 answered in 2b680cc (round 2 evidence: https://github.com/ToyOSOrg/evidence/tree/b8aca80f4d52e1e59593fadcf9de0836249ca73f/combined/r2).

  • BLOCKER 1: fixed, by the orchestrator's ruling. Protection goes where the operation is secret by construction.
    • siphash24 and ct::equal take the guard themselves, which covers netstack's keyed SipHash and wlan's MIC and key comparisons.
    • The plain hash takes none. The guard is exported as toyos_crypto::timing::protect, and toyos-ssh takes it in kex::Exchange::hash (H over K) and in kex::derive (RFC 4253 §7.2).
    • I searched every toyos_crypto::hash user outside the crate. The only other secret-hashing sites are test-only: bcachefs's crypt::oracle and sshserver's test client.
    • The crate's timing doc, the crypto track, the commit and the PR body now state the boundary. The unmet caller duty is gone.
  • BLOCKER 2: this is the orchestrator's bench run, queued (T14 then AMD, with the A/B).
  • Suggestions:
    • Taken:
      • x86's Mode is uninhabited, so it has no settable mode.
      • The MSR comment now states that register-only work may still move across it.
      • The ABI doc is reduced to the FEAT_DIT fact plus a pointer.
      • The FSFP record says the T14 enumerates PSFD.
    • Not taken: guest-target clippy for the one-line cfg(target_os = "toyos") use. That arm is compiled with warnings denied in every ToyOS guest build.
  • Checks at 2b680cc:
    • --build-only for x86-64 and aarch64: exit 0 each.
    • virt_smp and virt_el1_smp: exit 0, with 2021 and 1578 switches between states and 8 migrated threads.
    • cargo run -- --ci host: exit 0, 108 steps.
    • Not rerun:
      • The kernel IRQ control: the kernel did not change.
      • The QEMU x86 crypto suites: off ToyOS the new guards are nothing, and host CI ran the crate's tests.

@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1063 at 2b680cc

This round covers only the r1 fix commit 2b680cc (+83/−57 lines over fcd1e37). The base is still main 190f3b1. Blocker 1 is judged under the orchestrator's ruling: the guard sits in primitives that are secret by construction, and a plain hash takes none. Settled points are not reopened, including the declined guest-clippy suggestion.

Blocker 1: resolved

  • The guard in the primitives. siphash24 (lib/crypto/src/siphash.rs:27) and ct::equal (lib/crypto/src/ct.rs:31) now take the guard on their first line. HMAC, HKDF and PBKDF2 already took it (kdf.rs:60-164), as did the WPA2 PRF, hmac_sha1 and PMK, and the AEAD, ChaCha, Poly1305, scrypt and key operations.
    • ct::equal is now called inside already-protected Hmac::verify and the AEAD open. There the guard nests at the cost of one MRS. On x86, mode() is a constant None.
  • The plain hash takes none. hash.rs has no guard.
  • The export. pub mod timing re-exports toyos::timing::{protect, Protection} on ToyOS and provides a zero-sized stub elsewhere. Every other toyos::timing item keeps its SDK home.
  • lib/ssh. kex::Exchange::hash (lib/ssh/src/kex.rs:136) and kex::derive (:158) take toyos_crypto::timing::protect() as let _protection. These are the only places that hash K.
  • The records state the boundary. The crate's module doc and the crypto track now state it, and the unmet caller duty is gone.

Tree-wide search for misses. I listed every non-test file outside lib/crypto that uses toyos_crypto, and checked each one's hashing, MAC and comparison sites:

consumer secret work covered by
lib/ssh H over K, §7.2 derivation explicit guard (new)
lib/ssh x25519, ML-KEM, hybrid::for_ssh, ChaCha20-Poly1305 seal and open inside the primitives
lib/ssh auth and host key Ed25519 from_seed/sign guarded; verification is public
system/sshserver none outside tests its SHA-256 and K derivation are in connection.rs's #[cfg(test)] mod tests (line 497 onward)
netstack (ip/draw.rs, ip/limiter.rs, tcp ISN, timestamp offset, port offset and index via wire::siphash::low32) keyed SipHash inside siphash24
wlan handshake MIC (hmac_sha1 + ct::equal), PTK prf, PMK, KEK unwrap, GTK/IGTK reinstall compare (ct::equal) inside the primitives
bcachefs ChaCha, Poly1305 tag and verify_truncated, scrypt, dirent SipHash (read.rs:790) inside the primitives
bcachefs crypt::oracle (SHA-256 of the master key) — #[cfg(test)] (crypt.rs:183)
TLS (rustls_provider) HMAC sign_concat guarded; HKDF via kdf; AEAD and kx via the primitives transcript hash is public. Consumers are host build tools and osbench only
update, swap, pkg, system/update install, icon SHA-256/512 of public artefacts, signature verify public

I found no third-party hash, MAC or comparison crate in any manifest. I found no non-test caller that hashes or compares a secret without the guard.

Optional changes taken: checked

  • Single home for the DIT description. The ABI doc is reduced to the FEAT_DIT fact plus a pointer (sdk/abi/src/syscall.rs:2620). The save/restore invariant lives only at control_regs::user_timing_control. The SDK module doc carries the user contract. Each remaining text belongs to its layer. Accepted.
  • Uninhabited x86 Mode (sdk/toyos/src/arch/x86_64.rs:42).
    • enum Mode {} with match self {} leaves x86 nothing to set, and Protection.restore: Option<Mode> is always None.
    • On AArch64, Mode(()) has a private field and is built only in mode() after FEAT_DIT is known.
    • So the r1 unreachable! is gone by construction.
  • The nomem comment (sdk/toyos/src/arch/aarch64.rs:122-124) now states the limit in one clause. Accepted.
  • The FSFP record (issues/intels-store-forwarding-predictor-times-constant-time-code-by-its-data.md:23-24) now says the T14 enumerates PSFD (EDX 0x1). The ambiguity is gone.

Blocker 2: open, and not reviewed here

The metal cryptoctcurvescase on the T14 and the AMD, plus the cryptocase A/B, are still owed. This is the orchestrator's run. Nothing in 2b680cc touches x86 P-256 or constant-time code. The guards added to ct::equal and siphash24 compile to nothing on x86 ToyOS, so the images staged at 9a5d4fa still measure this head's x86 code.

Evidence

combined/r2 at evidence b8aca80f:

check at 2b680cc result
--build-only x86-64 exit 0
--build-only aarch64 exit 0
virt_smp / virt_el1_smp exit 0, with "2021 / 1578 switches between states, 8 migrated; nesting and early exits restore it"
--ci host exit 0

That is sufficient for this commit:

  • The kernel is unchanged, so the r1 IRQ control stands.
  • lib/ssh's two new guards are established by inspection, and no test is needed for them.

Suggestions (optional)

  • lib/crypto/src/timing.rs:15,19: both /// Off ToyOS: nothing. comments repeat the module doc's first line. Delete them.
  • lib/crypto/src/timing.rs:12: the off-ToyOS Protection has no is_active, but ToyOS's does. A portable caller that calls it would build only on ToyOS. No caller does. Either drop Protection from the export, since protect()'s return type is nameable without it, or give the stub is_active() -> false.
  • lib/crypto/src/timing.rs:5: "every operation that takes a key … on its first line" is slightly broader than the code. Some operations take a key and only copy or clamp it, and take no guard: x25519::SecretKey::from_bytes, the ChaCha20-Poly1305 constructors, ssh::SealingKey::new and Hkdf::from_prk. "Every operation that computes on a secret" would be exact.
  • FSFP record line 24: the edited line runs past the file's wrap width. Rewrap it.
  • Out of scope, possibly worth recording: lib/ssh/src/wire.rs:208 put_mpint scans K for leading zeros (transport.rs:349) outside any guard, and the length of K's encoding feeds H. This is RFC 8731's mpint encoding of K and predates this PR. DIT would not change a branch, so it is not a miss under the ruling.

SEND BACK. This is only for Blocker 2, the orchestrator's metal run. No code change is required. With its verdicts recorded and passing, this is LAND.

Japabu added a commit that referenced this pull request Nov 1, 2026
…ard's doc is exact

`put_mpint` found K's first nonzero byte with `position`, a branch on
each secret byte, before hashing K into H and every session key. It
now reads every byte and counts the leading zeros and the sign bit
arithmetically. The encoding's length still tells how many leading zero
bytes K has, as RFC 4251's mpint, which RFC 8731 prescribes for K,
publishes to whoever sees its hash time. Three RFC-shaped cases join
the test: a zero after the first digit, a sign byte after leading
zeros, and the empty integer. Dropping the first-nonzero condition from
the sign rule reds it.

Also: toyos-crypto's off-ToyOS `Protection` answers `is_active` as the
SDK's does, so a portable caller builds everywhere; the boundary reads
"every operation that computes on a secret", which is what the code
does (constructors that only store a key take none); the duplicate
stub comments are gone, and the FSFP record is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 2, 2026
ECDH and ECDSA verification ran every P-256 field multiplication through
a CPU feature check, with both operands passed through memory, and added
and subtracted in Rust whose carries compiled to flag materialisation.
Now the arithmetic is a property of the modulus type:

- `Modulus` carries the runtime operations (add, double, sub, mul,
  square, select), generic by default; `P256Field` overrides them with
  its word by word forms. `Residue` calls them, with no per-operation
  branch.
- `Modulus::dispatch` runs a `Computation` (a visitor generic over the
  modulus type) on the fastest arithmetic for that p. `P256Field` sends
  it to `arch::p256`, which checks CPUID once and runs it on
  `P256Kernels` where BMI2 and ADX are admitted. That type is private to
  arch/x86_64.rs, so nothing names the kernels without the check.
- The kernels take and return their words in registers. Multiplication
  holds a and b0 (in RDX) in registers and reads one word of b per row
  from row 1, forming q p3 by shifts, as a template has 13 general
  registers plus RDX and none is left for p3. Holding b instead and
  reading a from memory was faster in an isolated chain but slower in
  the AMD bench's formulas, where the memory operand was just stored:
  store-to-load forwarding sits on the critical path, slower on Zen 2
  than on Tiger Lake. Squaring reduces by mulx with p3 in a1's register
  once a1 is squared. Final subtractions are `cmov` or a mask from the
  borrow.
- The 16-entry masked table reads, of the variable-base table and of the
  comb's rows, go through `Modulus::select`: portable `ct` masks by
  default, and on AVX2 one vector per residue, kept or replaced under a
  vector equality of the index with a count of the entries. The mask is
  opaque to LLVM, so the select stays vpand/vpandn/vpor and does not
  become vblendvpd. The loads are `lddqu`: `_mm256_loadu_si256` copies
  through the stack under debug assertions.
- `Point` and `Affine` take the modulus type as a defaulted parameter, so
  agreement, public-key derivation and verification are each one
  dispatched computation; static tables stay in `P256Field`'s type and
  cast per read.

Also, from the same analysis:

- The variable-base table takes each even multiple by doubling and each
  odd one by one addition: 8 doublings and 7 additions, not 1 and 14.
- Inversion modulo a p of at most 256 bits takes divsteps with delta
  starting at 1/2: 590 steps suffice there (Wuille, libsecp256k1's
  safegcd_implementation.md), 10 batches of 59 against 12 of 62. The
  batch matrix is scaled to 2^62 whatever its step count, so applying it
  is unchanged. P-384 keeps delta = 1 and the paper's bound.
- Verification inverts its public s in variable time: runs of zeros at
  once, six bits cleared per multiple of f, stopping when g is zero.

On the Linux probe against main 2a5b446 in the same runs, ECDH went
from 78.6 to 63.1 us on the T14 and 70.8 to 53.1 on the AMD bench, and
ECDSA-P256 verification from 65.2 to 54.8 and 60.0 to 47.0 (#1063's
evidence).

Tests: each batch variant against the divstep definition; the public
inverse against integers; the point checks in the dispatched arithmetic;
the dispatch's choice against CPUID; and the P-256 differential,
Wycheproof, CAVP and DER tests now run in the dispatched form too, which
is no longer the portable code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 2, 2026
…ruction

The boundary is the operation that takes a secret. Keyed SipHash and
`ct::equal` now take the thread's protection themselves, as the ciphers,
MACs, KDFs and key operations already did; netstack's keyed SipHash and
wlan's MIC and key comparisons are covered through them. A plain hash
serves public data as often as secret and takes none, so the guard is
exported as `toyos_crypto::timing::protect` for a caller hashing a
secret: `toyos-ssh`'s exchange hash over K and its RFC 4253 §7.2 key
derivation take it. No other caller outside the crate hashes a secret
outside a test. The crate's module doc and the crypto track state that
boundary, and the caller duty no caller met is gone.

Also, from the review's suggestions:
- x86-64's timing mode is an uninhabited `Mode`, so nothing there can
  set one; AArch64's `Mode` exists only once FEAT_DIT is known.
- The DIT write says that only memory accesses are ordered by it.
- `SYS_TIMING_CONTROL`'s doc points at `toyos::timing`; the save and
  restore invariant stays at the kernel's `user_timing_control`.
- The FSFP record says the T14 enumerates PSFD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 2, 2026
…ard's doc is exact

`put_mpint` found K's first nonzero byte with `position`, a branch on
each secret byte, before hashing K into H and every session key. It
now reads every byte and counts the leading zeros and the sign bit
arithmetically. The encoding's length still tells how many leading zero
bytes K has, as RFC 4251's mpint, which RFC 8731 prescribes for K,
publishes to whoever sees its hash time. Three RFC-shaped cases join
the test: a zero after the first digit, a sign byte after leading
zeros, and the empty integer. Dropping the first-nonzero condition from
the sign rule reds it.

Also: toyos-crypto's off-ToyOS `Protection` answers `is_active` as the
SDK's does, so a portable caller builds everywhere; the boundary reads
"every operation that computes on a secret", which is what the code
does (constructors that only store a key take none); the duplicate
stub comments are gone, and the FSFP record is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 2, 2026
…nches

#1063's crypto_secret_timing_curves boots at 9a5d4fa: every routine passes
on the T14 and the AMD bench, controls held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 2, 2026
Three base/head pairs a bench put P-256 ECDH at 0.75-0.82 of main and
verification at 0.76-0.85. AMD asserted chacha20-poly1305/key rose
0.7-1.6 ns in all three with ring unchanged; its instructions are the
same in both builds and only its constructor moved within its cache
line, so it is recorded as a measured layout effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu marked this pull request as ready for review November 2, 2026 06:40
@Japabu
Japabu enabled auto-merge November 2, 2026 06:40
Japabu and others added 7 commits November 2, 2026 07:40
ECDH and ECDSA verification ran every P-256 field multiplication through
a CPU feature check, with both operands passed through memory, and added
and subtracted in Rust whose carries compiled to flag materialisation.
Now the arithmetic is a property of the modulus type:

- `Modulus` carries the runtime operations (add, double, sub, mul,
  square, select), generic by default; `P256Field` overrides them with
  its word by word forms. `Residue` calls them, with no per-operation
  branch.
- `Modulus::dispatch` runs a `Computation` (a visitor generic over the
  modulus type) on the fastest arithmetic for that p. `P256Field` sends
  it to `arch::p256`, which checks CPUID once and runs it on
  `P256Kernels` where BMI2 and ADX are admitted. That type is private to
  arch/x86_64.rs, so nothing names the kernels without the check.
- The kernels take and return their words in registers. Multiplication
  holds a and b0 (in RDX) in registers and reads one word of b per row
  from row 1, forming q p3 by shifts, as a template has 13 general
  registers plus RDX and none is left for p3. Holding b instead and
  reading a from memory was faster in an isolated chain but slower in
  the AMD bench's formulas, where the memory operand was just stored:
  store-to-load forwarding sits on the critical path, slower on Zen 2
  than on Tiger Lake. Squaring reduces by mulx with p3 in a1's register
  once a1 is squared. Final subtractions are `cmov` or a mask from the
  borrow.
- The 16-entry masked table reads, of the variable-base table and of the
  comb's rows, go through `Modulus::select`: portable `ct` masks by
  default, and on AVX2 one vector per residue, kept or replaced under a
  vector equality of the index with a count of the entries. The mask is
  opaque to LLVM, so the select stays vpand/vpandn/vpor and does not
  become vblendvpd. The loads are `lddqu`: `_mm256_loadu_si256` copies
  through the stack under debug assertions.
- `Point` and `Affine` take the modulus type as a defaulted parameter, so
  agreement, public-key derivation and verification are each one
  dispatched computation; static tables stay in `P256Field`'s type and
  cast per read.

Also, from the same analysis:

- The variable-base table takes each even multiple by doubling and each
  odd one by one addition: 8 doublings and 7 additions, not 1 and 14.
- Inversion modulo a p of at most 256 bits takes divsteps with delta
  starting at 1/2: 590 steps suffice there (Wuille, libsecp256k1's
  safegcd_implementation.md), 10 batches of 59 against 12 of 62. The
  batch matrix is scaled to 2^62 whatever its step count, so applying it
  is unchanged. P-384 keeps delta = 1 and the paper's bound.
- Verification inverts its public s in variable time: runs of zeros at
  once, six bits cleared per multiple of f, stopping when g is zero.

On the Linux probe against main 2a5b446 in the same runs, ECDH went
from 78.6 to 63.1 us on the T14 and 70.8 to 53.1 on the AMD bench, and
ECDSA-P256 verification from 65.2 to 54.8 and 60.0 to 47.0 (#1063's
evidence).

Tests: each batch variant against the divstep definition; the public
inverse against integers; the point checks in the dispatched arithmetic;
the dispatch's choice against CPUID; and the P-256 differential,
Wycheproof, CAVP and DER tests now run in the dispatched form too, which
is no longer the portable code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…secret work under it

`toyos::timing::protect()` turns on the CPU's data-independent timing
mode for the calling thread where the thread can set one, and answers a
`Protection` that puts back the state it found when it drops: protections
nest, an early return or an unwinding panic restores the state, and the
guard is not `Send`. Any program may use it; it asks for no right.

- AArch64: `PSTATE.DIT` (FEAT_DIT). EL0 writes it, every exception from
  EL0 saves it in `SPSR_EL1` in the thread's trap frame and the return
  puts it back, and a thread starts with `SPSR_EL1` zero, so it is the
  thread's own across switches and CPUs with no switch work. The SDK
  reads and writes it with `MRS`/`MSR` once one `SYS_TIMING_CONTROL` has
  said every CPU has FEAT_DIT, which EL0 cannot read itself.
  `control_regs::check` holds every CPU's FEAT_DIT to the boot CPU's. The
  write is not `nomem`, so no memory access of the bracketed work moves
  across it.
- x86-64: `SYS_TIMING_CONTROL` answers false and every protection is
  inactive. No mode there is a thread's to set: Intel's DOITM is Ring 0's,
  and by the owner's ruling (2026-10-26) ToyOS switches it for no thread,
  so constant time there rests on the code, as on AMD, which has no such
  mode. The T14 finding that ruling weighed is recorded in
  issues/intels-store-forwarding-predictor-times-constant-time-code-by-its-data.md.

`timing_protection`, on virt_smp and virt_el1_smp's eight emulated CPUs,
reads each thread's `PSTATE.DIT` with `MRS`: nesting, `?` and a caught
panic restore it, toyos-crypto's own protection is on inside P-256's
entropy callback, and two threads a CPU, alternately protected, read
their own state 200 times while the diary shows migrations and switches
between the states.

toyos-crypto:

On ToyOS the first line of each public operation on a secret takes
`toyos::timing`'s protection, which ends with the operation and nests in
any its caller holds: AEAD key setup, seal and open; ChaCha20; Poly1305;
HMAC, HKDF, PBKDF2 and scrypt; Ed25519 key derivation and signing;
X25519 and the NIST curves' key generation, public key and agreement;
ML-KEM generation, encapsulation and decapsulation; the hybrid
combiners; OpenSSH's chacha20-poly1305; WPA2's derivations; and the
rustls provider's HMAC. Verification, hashes of public data, SipHash and
`ct`'s comparisons take none. Off ToyOS the protection is nothing.

A P-256 agreement, public key or generation is one computation that
`Modulus::dispatch` runs on the register kernels or the generic
arithmetic, so the whole of it runs inside its entry's protection. On
AArch64 that is `PSTATE.DIT`; on x86-64, where ToyOS has no mode, the
protection is a constant inactive guard, so the kernels' speed is
untouched and their constant time is their code's.

The crate gains `toyos` as a dependency on target_os = "toyos" alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its own

The owner confirmed the 2026-10-26 ruling: no DOITM on x86-64, constant
time there in software. Its two residuals are each a recorded issue: the
T14's store-forwarding predictor, and Intel's MXCSR-dependent timing on
the AVX2 Poly1305's VPMULUDQ, which the crypto track now cites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ruction

The boundary is the operation that takes a secret. Keyed SipHash and
`ct::equal` now take the thread's protection themselves, as the ciphers,
MACs, KDFs and key operations already did; netstack's keyed SipHash and
wlan's MIC and key comparisons are covered through them. A plain hash
serves public data as often as secret and takes none, so the guard is
exported as `toyos_crypto::timing::protect` for a caller hashing a
secret: `toyos-ssh`'s exchange hash over K and its RFC 4253 §7.2 key
derivation take it. No other caller outside the crate hashes a secret
outside a test. The crate's module doc and the crypto track state that
boundary, and the caller duty no caller met is gone.

Also, from the review's suggestions:
- x86-64's timing mode is an uninhabited `Mode`, so nothing there can
  set one; AArch64's `Mode` exists only once FEAT_DIT is known.
- The DIT write says that only memory accesses are ordered by it.
- `SYS_TIMING_CONTROL`'s doc points at `toyos::timing`; the save and
  restore invariant stays at the kernel's `user_timing_control`.
- The FSFP record says the T14 enumerates PSFD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ard's doc is exact

`put_mpint` found K's first nonzero byte with `position`, a branch on
each secret byte, before hashing K into H and every session key. It
now reads every byte and counts the leading zeros and the sign bit
arithmetically. The encoding's length still tells how many leading zero
bytes K has, as RFC 4251's mpint, which RFC 8731 prescribes for K,
publishes to whoever sees its hash time. Three RFC-shaped cases join
the test: a zero after the first digit, a sign byte after leading
zeros, and the empty integer. Dropping the first-nonzero condition from
the sign rule reds it.

Also: toyos-crypto's off-ToyOS `Protection` answers `is_active` as the
SDK's does, so a portable caller builds everywhere; the boundary reads
"every operation that computes on a secret", which is what the code
does (constructors that only store a key take none); the duplicate
stub comments are gone, and the FSFP record is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nches

#1063's crypto_secret_timing_curves boots at 9a5d4fa: every routine passes
on the T14 and the AMD bench, controls held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three base/head pairs a bench put P-256 ECDH at 0.75-0.82 of main and
verification at 0.76-0.85. AMD asserted chacha20-poly1305/key rose
0.7-1.6 ns in all three with ring unchanged; its instructions are the
same in both builds and only its constructor moved within its cache
line, so it is recorded as a measured layout effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit e3a4215 Nov 3, 2026
5 checks passed
@Japabu
Japabu deleted the p256-speed branch November 3, 2026 19:56
Japabu added a commit that referenced this pull request Nov 3, 2026
Main took syscall 165 for SYS_TIMING_CONTROL (#1063). This branch's
SYS_NAMESPACE_NAMES stays 166 above it, with 168 profile bins, and the
one bound assertion names the highest number.

Merged rather than rebased: the branch carries the merge commits that
joined #1141, #1144 and the integration's own lines, and a linear
rebase would replay their merged-in commits onto bases they were never
written for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 3, 2026
Resolution: sdk/abi/src/syscall.rs keeps main's SYS_TIMING_CONTROL at 165;
SYS_DEVICE_SET is 166 and SYS_DEVICE_SET_READ 167, with 169 profile bins
and the assert on 167. Everything else merged cleanly.

Co-Authored-By: Claude Opus 5.5 <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

Development

Successfully merging this pull request may close these issues.

1 participant