Repository navigation
toyos-crypto: P-256 on register kernels, and a thread's own timing protection (PSTATE.DIT) around secret work - #1063
Conversation
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>
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 ArchitectureThe split of responsibilities is right, and it is small:
I checked the kernel side:
The guard's semantics are sound:
On x86-64 the guard is a constant inactive value, as ruled. P-256 on x86 (
The Intel guidance confirms the MCDT record:
No other MCDT-listed instruction occurs in 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. FindingsBLOCKER 1 — the protection's coverage boundary leaves real secret work unprotected, and the contract's caller duty is one no caller meets.
BLOCKER 2 — the constant-time evidence for the new x86 kernels is owed.
SUGGESTION — the
SUGGESTION — the DIT mechanism is described in four places.
SUGGESTION — make the x86
SUGGESTION — the FSFP record's PSFD sentence is ambiguous.
SUGGESTION — the new
Tests and evidenceChecked in the implementer's logs:
The guest test is focused and justified, its premises are checked in the trace diary, and its targets are declared through The NIST log ends after 4 of its 5 tests with SEND BACK |
…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>
|
Review r1 answered in 2b680cc (round 2 evidence: https://github.com/ToyOSOrg/evidence/tree/b8aca80f4d52e1e59593fadcf9de0836249ca73f/combined/r2).
|
Review r2 of #1063 at 2b680ccThis 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
Tree-wide search for misses. I listed every non-test file outside
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
Blocker 2: open, and not reviewed hereThe metal Evidence
That is sufficient for this commit:
Suggestions (optional)
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. |
…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>
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>
…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>
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>
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>
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>
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>
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>
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
Change
P-256 dispatches once per computation, on register kernels (commit 1)
This is #1063's code unchanged: the patch-id of
origin/main..98d3bebf3equals that of #1063's2a5b446b0..9dc8a7771. Its three rounds are squashed into one commit, so the swap that round 3 undid is not in the history.Modulusholds add, double, sub, mul, square and select.Modulus::dispatchruns aComputationonce:P256Fieldsends it toarch::p256, which checks CPUID once.P256Kernels. That type is private toarch/x86_64.rs.mulx.cmovor borrow masks.vpand/vpandn/vporunder an opaque mask.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!SendProtectionthat puts back the state it found:AArch64:
PSTATE.DIT. EL0 writes it. Every exception from EL0 saves it inSPSR_EL1in 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'sSYS_DEVICE_FIRMWARE_WINDOW) only tells a program that every CPU has FEAT_DIT, which EL0 cannot read itself.control_regs::checkholds every CPU's FEAT_DIT to the boot CPU's.MRS/MSRdirectly. The write is notnomem, so the compiler cannot move the protected work's memory accesses out of the protected span.x86-64: no mode.
SYS_TIMING_CONTROLanswers false and everyProtectionis 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.
ct::equal, added in review r1;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.
toyos_crypto::timing::protectfor a caller that hashes a secret.toyos-ssh's exchange hash over K and its RFC 4253 §7.2 key derivation take it.put_mpintencodes 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.hash::user outside the crate; bcachefs's and sshserver's are test-only.Records.
issues/intels-store-forwarding-predictor-times-constant-time-code-by-its-data.mdrecords the T14 finding, with evidence and an exit.issues/the-avx2-poly1305-runs-under-intels-mxcsr-dependent-timing.mdrecords Intel's MXCSR-dependent timing (MCDT) on the AVX2 Poly1305'sVPMULUDQ. The T14 does not report itself immune.How the two relate
Modulus::dispatchruns. The protection is taken at the public entry, so the whole computation runs inside it.PSTATE.DIT.crypto_secret_timing_curveson both benches checks that.Decisions to review
PSTATE.DIT. This PR builds exactly that.issues/intels-store-forwarding-predictor-times-constant-time-code-by-its-data.md;issues/the-avx2-poly1305-runs-under-intels-mxcsr-dependent-timing.md.IA32_UARCH_MISC_CTL. The register resets to 0 (Intel 768873) and ToyOS never writes it.Checks (evidence: https://github.com/ToyOSOrg/evidence/tree/f5943573d942d8f78edb4f6d9420592c12119f43/combined/r1)
-cpu max(BMI2/ADX/AVX2: the kernels), includingp256_computations_run_on_the_kernels_the_cpu_admitscargo test --test toyos-build -- virt_smp virt_el1_smp(AArch64, 8 emulated CPUs, FEAT_DIT)SPSR.DITon every EL0 interrupt returnSecretKey::generatetakes no protectioncargo run -- --build-only(x86-64) and--arch aarch64cargo run -- --ci host--build-onlyx86-64 and aarch64;virt_smpvirt_el1_smp(2021 / 1578 switches between states, 8 migrated);cargo run -- --ci hostcargo 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 warningsSPSR_EL1.DITsurvives every switch and migration. No host test reaches that. The job runs only on AArch64 (RUST_SKIPon x86-64, which has no such state).max, invirt_smpandvirt_el1_smp. No ARM bench exists. AArch64 without FEAT_DIT (cortex-a72) takes the same one-linecontrol_regsanswer and is not run.ct::equalandsiphash24. Off ToyOS the guard is nothing, so the host and Linux suites run the same code. On x86 ToyOS it is a constantNone.Modetoken. virt_smp's nesting, early-exit and panic asserts held at 2b680cc.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_instrumenton all eightcryptocasereadbacks, andcrypto_secret_timing_curveson both benches.Constant time: PASS on both benches. 1,000,000 measurements a routine, no CPU timing mode:
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:
P-384 is 0.95–0.99 everywhere except T14 unasserted pair 2.
chacha20-poly1305/key: 5.3 → 6.0 and 6.9 ns (1.13, 1.30), ring unchanged.SealingKey::chacha20_poly1305takes no guard, and its constructor andSecretwipe are unchanged.Settling the two cells (orchestrator's ruling: evidence, not normalisation)
chacha20-poly1305/key's loop, constructor and drop have identical instructions in base and head.SealingKey::chacha20_poly1305moved: 0xc9180 (64-byte aligned) to 0xf5360 (+32).chacha20-poly1305/keyrepeats: 5.3 → 6.0 ns (1.132), ring unchanged.Unresolved risks
VPMULUDQruns under the default MXCSR (the new issue).P256Kernelssoundness rests on type privacy and the dispatch check, not a compile-time token.🤖 Generated with Claude Code