From 3f96907fff2ad41ec9d529b106a98c82117ce505 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:34:57 +0900 Subject: [PATCH] build!: require Rust 1.84 and let lint exceptions expire Rust 1.84 has `#[expect]` with `reason =`, pointer `addr`, `&raw`, and a resolver that respects the MSRV, which Rust 1.75 lacks. The `unsafe_code` of `sys` and a probe's `process::exit` become expectations with their reasons, and the registry in `xtask/src/gates.rs` goes. Clippy refuses `#[allow]` and an exception without a reason, rustc fails an unmet expectation, and the gate refuses every `allow`. A handle converts through `addr` and `ptr::without_provenance_mut` in `domain::conversion_rules`, where Kani proves the round trip for every `isize`. Pointers to locals that Windows reads or writes use `&raw`, and the setters that only record a choice become `const fn`. CI checks Rust 1.84 in place of Rust 1.75, and the workspace uses resolver 3. A Renovate rule keeps the MSRV pin, which the organization's automerged minor updates would move to the newest release. ADR 0018 records the decision. --- .github/renovate.json | 11 ++ .github/workflows/ci.yml | 4 +- CHANGELOG.md | 2 + CONTRIBUTING.md | 4 +- Cargo.toml | 7 +- README.md | 2 +- ...pository-laws-are-enforced-mechanically.md | 16 +- .../0016-spawn-state-is-explicit-and-typed.md | 2 +- ...ptions-expire-and-the-msrv-is-rust-1-84.md | 35 ++++ docs/migration-0.2.md | 2 + proofs/README.md | 2 +- public-api/windows-spawn.txt | 18 +- src/child.rs | 2 +- src/command.rs | 18 +- src/domain/conversion_rules.rs | 27 ++- src/domain/creation_flag_rules.rs | 2 +- src/lib.rs | 5 +- src/mitigation.rs | 2 +- src/options.rs | 8 +- src/resource.rs | 2 +- src/sys.rs | 64 +++---- tests/argv_roundtrip.rs | 6 +- tests/command_line_parsers.rs | 13 +- tests/random/mod.rs | 2 +- tests/support/mod.rs | 15 +- tests/windows_spawn.rs | 46 +++-- xtask/Cargo.toml | 2 +- xtask/src/gates.rs | 175 ++++-------------- xtask/src/proofs/kani.rs | 4 + xtask/src/tasks.rs | 6 +- 30 files changed, 255 insertions(+), 249 deletions(-) create mode 100644 .github/renovate.json create mode 100644 docs/adr/0018-lint-exceptions-expire-and-the-msrv-is-rust-1-84.md diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..09ae8b6 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "packageRules": [ + { + "description": "A numeric Rust toolchain is the MSRV, which follows rust-version in Cargo.toml and moves only with an ADR (ADR 0018).", + "matchDepNames": ["rust"], + "matchCurrentValue": "/^\\d+\\.\\d+(\\.\\d+)?$/", + "enabled": false + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1d112e..85a7d52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 with: - toolchain: "1.75" + toolchain: "1.84" - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 with: toolchain: nightly-2026-07-02 @@ -52,7 +52,7 @@ jobs: fail-fast: false matrix: os: [windows-2022, windows-latest] - rust: [stable, "1.75"] + rust: [stable, "1.84"] runs-on: ${{ matrix.os }} timeout-minutes: 15 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2038d1e..bc872fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ The 0.2 API follows `std::process` and places each choice on the type that owns - `Job::assign` takes any process handle. - `Debug` prints a `Command` as std does. - `SuspendedChild::id` is `const`, like `Child::id`, and returns the identifier recorded at creation, so it cannot panic. +- The MSRV is Rust 1.84, as ADR 0018 records. +- `Command::own_process_tree`, `console`, `priority`, `mitigation`, and the creation-flag setters are `const fn`. ### Removed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9bbdd20..4fddfa7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ A change to ownership, handle inheritance, Job lifetime, ConPTY, mitigation, quo ## Development Tests require Windows 10 version 1809 or later. -The crate supports Rust 1.75 and later. +The crate supports Rust 1.84 and later. With the tool versions CI uses installed, run: ```powershell @@ -67,7 +67,7 @@ cargo +nightly fuzz run environment-block -- -max_total_time=600 -max_len=16384 - Keep the documented ownership and cleanup behavior, including on errors. - Give every `unsafe` block a specific safety justification. - Write only doc comments and `// SAFETY:` comments; the reason for a change goes in its commit message (ADR 0013). -- Register every `#[allow]` in `xtask/src/gates.rs` with its reason; `just gates` checks the registry and the other repository laws. +- Write a lint exception as `#[expect(lint, reason = "…")]` on the one item it concerns; `just gates` refuses every `allow` and checks the other repository laws (ADR 0018). - Add deterministic tests for behavior changes; tests wait for events, never for time (ADR 0010). - Change the public API snapshot only with an API change, and state its compatibility impact. - Update the crate docs, ADRs, or security boundary when a contract changes. diff --git a/Cargo.toml b/Cargo.toml index 582bb43..b46c527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "windows-spawn" version = "0.1.0" authors = ["Yasunobu Sakashita"] edition = "2021" -rust-version = "1.75" +rust-version = "1.84" license = "MIT OR Apache-2.0" description = "Windows process creation with explicit handle, Job, mitigation, ConPTY, and child ownership" documentation = "https://docs.rs/windows-spawn" @@ -62,7 +62,7 @@ features = ["Win32_System_LibraryLoader", "Win32_UI_Shell"] [workspace] members = ["xtask"] default-members = ["."] -resolver = "2" +resolver = "3" [workspace.lints.rust] future_incompatible = { level = "deny", priority = -1 } @@ -80,6 +80,7 @@ trivial_numeric_casts = "deny" unreachable_pub = "deny" unsafe_op_in_unsafe_fn = "deny" unused_lifetimes = "deny" +unfulfilled_lint_expectations = "deny" unused_qualifications = "deny" variant_size_differences = "deny" unexpected_cfgs = { level = "deny", check-cfg = ["cfg(fuzzing)", "cfg(kani)", "cfg(windows_spawn_vectors)"] } @@ -89,6 +90,8 @@ all = { level = "deny", priority = -1 } cargo = { level = "deny", priority = -1 } nursery = { level = "deny", priority = -1 } pedantic = { level = "deny", priority = -1 } +allow_attributes = "deny" +allow_attributes_without_reason = "deny" arithmetic_side_effects = "deny" as_conversions = "deny" clone_on_ref_ptr = "deny" diff --git a/README.md b/README.md index ce09544..3baeb0e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ process trees that end with their owner, Jobs, pseudoconsoles, handle handoff, s Use `std::process::Command` for portable child processes. Use this crate when a Windows program needs one of these, or a spawn that passes the child no stray handle and releases everything when a step fails. -Requires Windows 10 version 1809 or later and Rust 1.75 or later. +Requires Windows 10 version 1809 or later and Rust 1.84 or later. Non-Windows targets expose no API. Not affiliated with Microsoft or `windows-rs`; `windows-` names the target platform. diff --git a/docs/adr/0013-repository-laws-are-enforced-mechanically.md b/docs/adr/0013-repository-laws-are-enforced-mechanically.md index 385121a..b1880f9 100644 --- a/docs/adr/0013-repository-laws-are-enforced-mechanically.md +++ b/docs/adr/0013-repository-laws-are-enforced-mechanically.md @@ -1,11 +1,11 @@ # 0013 — Repository laws are enforced mechanically -Status: accepted (2026-09-25); ADR 0016 extends the trait-object rule to every `dyn` +Status: accepted (2026-09-25); ADR 0016 extends the trait-object rule to every `dyn`, and ADR 0018 replaces the exception registry with `#[expect]` ## Context Rules kept by review alone erode: timed waits, explanatory comments, and silent conversions came back after they were removed. -Rust 1.75, the MSRV, has neither `#[expect]` nor `reason =`, so a lint exception can neither state its reason nor prove it is still needed. +Rust 1.75, the MSRV then, had neither `#[expect]` nor `reason =`, so a lint exception could neither state its reason nor prove it was still needed. ## Decision @@ -19,9 +19,8 @@ Every law has a mechanism, and CI runs it. `cargo xtask gates` bans time-named identifiers, delay commands in strings, and `WaitForSingleObject` with a bound other than `INFINITE` or `0`. - Reasons belong in commit messages. The gate allows doc comments and whole-line `// SAFETY:` runs, and nothing else. -- Every lint exception is registered. - The gate collects each `#[allow]` lint and requires an entry with the same file, lint, and count, with a reason, in `xtask/src/gates.rs`. - An entry no attribute uses also fails. +- Every lint exception states its reason and expires (ADR 0018). + It is an `#[expect(…, reason = …)]` on the one item it concerns, and the gate refuses every `allow`. - Defaults are chosen explicitly. The gate bans `#[default]`; a documented `impl Default` states the choice. - Types are closed or generic. @@ -30,12 +29,9 @@ Every law has a mechanism, and CI runs it. `clippy.toml` bans `mem::forget`, `process::exit`, `env::set_var`, and `env::remove_var`. The gate is a lexer in xtask with no dependencies, because `syn` needs `unicode-ident`, whose license `deny.toml` does not allow. -Rustc lints are limited to those Rust 1.75 recognizes. +Rustc lints are limited to those the MSRV recognizes. ## Consequences - Changing a law changes the lint table, `clippy.toml`, or the gate, with its tests. -- Reading a handle's value keeps `as` behind a registered exception in `sys`, because Rust 1.75 has no pointer `addr`. - A value becomes a handle through a byte offset from null. - A `DWORD` width converts through its bytes in `domain::conversion_rules`, where Kani proves each conversion for every input. -- When the MSRV reaches Rust 1.81, `#[expect(…, reason = …)]` replaces the registry. +- A conversion that `From` lacks goes through bytes or a pointer's address in `domain::conversion_rules`, where Kani proves it for every input, so no `as` remains. diff --git a/docs/adr/0016-spawn-state-is-explicit-and-typed.md b/docs/adr/0016-spawn-state-is-explicit-and-typed.md index d11634e..55f6146 100644 --- a/docs/adr/0016-spawn-state-is-explicit-and-typed.md +++ b/docs/adr/0016-spawn-state-is-explicit-and-typed.md @@ -34,7 +34,7 @@ Main dropped #14, and with it those rules. ## Consequences -- The registry holds no `expect_used` or `too_many_lines` exception for `src/child.rs` or `src/transaction.rs`. +- No lint exception covers `expect_used` or `too_many_lines` in `src/child.rs` or `src/transaction.rs`. - A handle of the wrong kind, a value of the wrong table, or an attribute value that dies before its list fails to compile. - Concurrency in the library needs a typed boundary and a model before it can pass the gate. - The output readers of `Child::wait_with_output` stay threads that own what they read, so they share no state. diff --git a/docs/adr/0018-lint-exceptions-expire-and-the-msrv-is-rust-1-84.md b/docs/adr/0018-lint-exceptions-expire-and-the-msrv-is-rust-1-84.md new file mode 100644 index 0000000..31effda --- /dev/null +++ b/docs/adr/0018-lint-exceptions-expire-and-the-msrv-is-rust-1-84.md @@ -0,0 +1,35 @@ +# 0018 — Lint exceptions expire, and the MSRV is Rust 1.84 + +Status: accepted (2026-10-13) + +## Context + +Rust 1.75 has neither `#[expect]` nor `reason =`, so ADR 0013 registered every `#[allow]` in `xtask/src/gates.rs` with a reason and a count. +A registered exception could still outlive its need, and three remained: the `unsafe_code` of `sys`, the `as` that read a handle's value, and a probe's `process::exit`. +Rust 1.75 also has no pointer `addr`, so reading a handle's value took `as`, and the FFI calls borrowed each local as a reference before it became a pointer. +Rust 1.84 has `#[expect]` with `reason =`, `addr` and `ptr::without_provenance_mut`, `&raw mut`, and a resolver that keeps dependencies within the MSRV. + +## Decision + +- The MSRV is Rust 1.84. +- Every lint exception is an `#[expect(lint, reason = "…")]` on the one item it concerns. + Clippy's `allow_attributes` and `allow_attributes_without_reason` refuse an `#[allow]` and an exception without a reason. + `unfulfilled_lint_expectations` fails an exception the code no longer needs. + The gate refuses every `allow` it reads, including the inner attributes that Clippy skips, and the registry goes. +- A handle converts through its address alone. + `addr` reads its value, and `ptr::without_provenance_mut` makes one from a value, so Kani proves the round trip for every `isize`. +- An FFI call takes a pointer to a local as `&raw mut` or `&raw const`, which makes the pointer without an intermediate reference, as Clippy's `borrow_as_ptr` requires. +- The workspace uses resolver 3, so `cargo update` keeps the dependencies within the MSRV. +- `.github/renovate.json` keeps Renovate off the numeric Rust toolchain in CI, which is the MSRV. + The organization's preset automerges minor updates, and its open update moves the MSRV check from Rust 1.75 to the newest release. + +Rejected: + +- Rust 1.75 and its registry, which can't tell when an exception stops applying. +- Rust 1.81, which has `#[expect]` but not `addr`, so reading a handle's value would keep `as`. + +## Consequences + +- A project on a Rust older than 1.84 stays on windows-spawn 0.1. +- The `Command` setters that only record a choice are `const fn`, which Rust 1.83 allows for `&mut self`. +- No `allow` remains in the repository. diff --git a/docs/migration-0.2.md b/docs/migration-0.2.md index 903ce4b..052f6d4 100644 --- a/docs/migration-0.2.md +++ b/docs/migration-0.2.md @@ -4,6 +4,8 @@ Most programs change in three places: the error type, the tree policy, and the creation flags. [ADR 0015](adr/0015-the-api-follows-std-and-ownership.md) records why. +0.2 requires Rust 1.84, where 0.1 required Rust 1.75; [ADR 0018](adr/0018-lint-exceptions-expire-and-the-msrv-is-rust-1-84.md) records why. + ## Errors Fallible calls return `windows_spawn::Result`, whose error is `SpawnError`, instead of `std::io::Result`. diff --git a/proofs/README.md b/proofs/README.md index 184d592..eb946bb 100644 --- a/proofs/README.md +++ b/proofs/README.md @@ -6,7 +6,7 @@ Each layer runs a pinned tool, accepts only the complete expected result, and ru | Layer | Tool | What it proves | How it reaches the code | | --- | --- | --- | --- | -| `kani` | Kani 0.68.0 | The decision cores and `DWORD` conversions in `src/domain` and the mitigation layout, for every input within each harness's bounds | It runs the crate's own functions | +| `kani` | Kani 0.68.0 | The decision cores and Win32 conversions in `src/domain` and the mitigation layout, for every input within each harness's bounds | It runs the crate's own functions | | `lean` | Lean 4.35.0 | Both C runtime parsers read back every command line the encoder writes, for arguments of every length | Vectors from the proved model, which the Rust tests and the Windows parsers check | | `verus` | Verus 0.2026.10.04 | The encoder writes what the Lean model writes, for every argument | A copy that must equal the crate's functions token for token | | `tla` | `TLC` 1.7.4 | `Child::wait_with_output` returns with every byte, and each of two plausible mistakes deadlocks or never returns | A model of the protocol | diff --git a/public-api/windows-spawn.txt b/public-api/windows-spawn.txt index 2b85032..aec4cfa 100644 --- a/public-api/windows-spawn.txt +++ b/public-api/windows-spawn.txt @@ -686,10 +686,10 @@ impl windows_spawn::Command pub fn windows_spawn::Command::arg>(&mut self, S) -> &mut Self pub fn windows_spawn::Command::arg_handle(&mut self, &T) -> windows_spawn::Result<&mut Self> pub fn windows_spawn::Command::args(&mut self, I) -> &mut Self where I: core::iter::traits::collect::IntoIterator, S: core::convert::AsRef -pub fn windows_spawn::Command::breakaway_from_job(&mut self) -> &mut Self -pub fn windows_spawn::Command::console(&mut self, windows_spawn::ConsoleMode) -> &mut Self +pub const fn windows_spawn::Command::breakaway_from_job(&mut self) -> &mut Self +pub const fn windows_spawn::Command::console(&mut self, windows_spawn::ConsoleMode) -> &mut Self pub fn windows_spawn::Command::current_dir>(&mut self, P) -> &mut Self -pub fn windows_spawn::Command::default_error_mode(&mut self) -> &mut Self +pub const fn windows_spawn::Command::default_error_mode(&mut self) -> &mut Self pub fn windows_spawn::Command::env(&mut self, K, V) -> &mut Self where K: core::convert::AsRef, V: core::convert::AsRef pub fn windows_spawn::Command::env_clear(&mut self) -> &mut Self pub fn windows_spawn::Command::env_handle, T: std::os::windows::io::handle::AsHandle>(&mut self, K, &T) -> windows_spawn::Result<&mut Self> @@ -697,15 +697,15 @@ pub fn windows_spawn::Command::env_remove(&mut self, I) -> &mut Self where I: core::iter::traits::collect::IntoIterator, K: core::convert::AsRef, V: core::convert::AsRef pub fn windows_spawn::Command::get_current_dir(&self) -> core::option::Option<&std::path::Path> pub fn windows_spawn::Command::get_program(&self) -> &std::ffi::os_str::OsStr -pub fn windows_spawn::Command::inherit_parent_affinity(&mut self) -> &mut Self -pub fn windows_spawn::Command::mitigation(&mut self, windows_spawn::MitigationPolicy) -> &mut Self +pub const fn windows_spawn::Command::inherit_parent_affinity(&mut self) -> &mut Self +pub const fn windows_spawn::Command::mitigation(&mut self, windows_spawn::MitigationPolicy) -> &mut Self pub fn windows_spawn::Command::new>(S) -> Self -pub fn windows_spawn::Command::new_process_group(&mut self) -> &mut Self +pub const fn windows_spawn::Command::new_process_group(&mut self) -> &mut Self pub fn windows_spawn::Command::output(&mut self) -> windows_spawn::Result pub fn windows_spawn::Command::output_with(&mut self, windows_spawn::SpawnOptions<'_>) -> windows_spawn::Result -pub fn windows_spawn::Command::own_process_tree(&mut self) -> &mut Self -pub fn windows_spawn::Command::preserve_code_authorization(&mut self) -> &mut Self -pub fn windows_spawn::Command::priority(&mut self, windows_spawn::PriorityClass) -> &mut Self +pub const fn windows_spawn::Command::own_process_tree(&mut self) -> &mut Self +pub const fn windows_spawn::Command::preserve_code_authorization(&mut self) -> &mut Self +pub const fn windows_spawn::Command::priority(&mut self, windows_spawn::PriorityClass) -> &mut Self pub fn windows_spawn::Command::raw_arg>(&mut self, S) -> &mut Self pub fn windows_spawn::Command::spawn(&mut self) -> windows_spawn::Result pub fn windows_spawn::Command::spawn_suspended(&mut self) -> windows_spawn::Result diff --git a/src/child.rs b/src/child.rs index f5cbe66..88f9b93 100644 --- a/src/child.rs +++ b/src/child.rs @@ -170,7 +170,7 @@ impl ProcessHandle { } /// Records that the main thread runs, so dropping the handle leaves the process running. - pub(crate) fn mark_started(&mut self) { + pub(crate) const fn mark_started(&mut self) { self.unstarted = false; } diff --git a/src/command.rs b/src/command.rs index 231929f..867d74e 100644 --- a/src/command.rs +++ b/src/command.rs @@ -256,7 +256,7 @@ impl Command { /// The Job allows no breakaway, so a descendant that asks for `CREATE_BREAKAWAY_FROM_JOB` fails to start. /// A daemon that a build starts and expects to outlive it, such as `mspdbsrv.exe` or an `sccache` server, ends with the tree too. /// A [`SuspendedChild`] already runs in the Job, and dropping it terminates the child before it runs. - pub fn own_process_tree(&mut self) -> &mut Self { + pub const fn own_process_tree(&mut self) -> &mut Self { self.creation.owns_tree = true; self } @@ -264,7 +264,7 @@ impl Command { /// Chooses the child's console. /// /// A pseudoconsole from [`SpawnOptions::pseudo_console`] is the child's console, so it combines only with [`ConsoleMode::Inherit`]. - pub fn console(&mut self, mode: ConsoleMode) -> &mut Self { + pub const fn console(&mut self, mode: ConsoleMode) -> &mut Self { self.creation.console = mode; self } @@ -272,43 +272,43 @@ impl Command { /// Chooses the child's scheduling priority class. /// /// Without a choice, the child gets the normal class, or the caller's when that is idle or below normal. - pub fn priority(&mut self, class: PriorityClass) -> &mut Self { + pub const fn priority(&mut self, class: PriorityClass) -> &mut Self { self.creation.set_priority(class); self } /// Applies a process-creation mitigation policy: `PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY`. - pub fn mitigation(&mut self, policy: MitigationPolicy) -> &mut Self { + pub const fn mitigation(&mut self, policy: MitigationPolicy) -> &mut Self { self.creation.mitigation = policy; self } /// Makes the child the root of a new process group, which `GenerateConsoleCtrlEvent` can signal: `CREATE_NEW_PROCESS_GROUP`. - pub fn new_process_group(&mut self) -> &mut Self { + pub const fn new_process_group(&mut self) -> &mut Self { self.creation.set(flags::NEW_PROCESS_GROUP); self } /// Gives the child its logical parent's processor affinity: `INHERIT_PARENT_AFFINITY`. - pub fn inherit_parent_affinity(&mut self) -> &mut Self { + pub const fn inherit_parent_affinity(&mut self) -> &mut Self { self.creation.set(flags::INHERIT_PARENT_AFFINITY); self } /// Lets the child leave the caller's Job where that Job allows it: `CREATE_BREAKAWAY_FROM_JOB`. - pub fn breakaway_from_job(&mut self) -> &mut Self { + pub const fn breakaway_from_job(&mut self) -> &mut Self { self.creation.set(flags::BREAKAWAY_FROM_JOB); self } /// Keeps the caller's code-authorization level for the child: `CREATE_PRESERVE_CODE_AUTHZ_LEVEL`. - pub fn preserve_code_authorization(&mut self) -> &mut Self { + pub const fn preserve_code_authorization(&mut self) -> &mut Self { self.creation.set(flags::PRESERVE_CODE_AUTHZ_LEVEL); self } /// Starts the child with the default hard-error mode instead of the caller's: `CREATE_DEFAULT_ERROR_MODE`. - pub fn default_error_mode(&mut self) -> &mut Self { + pub const fn default_error_mode(&mut self) -> &mut Self { self.creation.set(flags::DEFAULT_ERROR_MODE); self } diff --git a/src/domain/conversion_rules.rs b/src/domain/conversion_rules.rs index b63fbc2..e6282c8 100644 --- a/src/domain/conversion_rules.rs +++ b/src/domain/conversion_rules.rs @@ -1,8 +1,11 @@ -//! Conversions between Win32 widths and Rust integers that `From` does not offer, written without `as`. +//! Conversions between Win32 values and Rust integers that `From` does not offer, written without `as`. //! //! A `DWORD` widens to a `usize`, and a compile-time size narrows to a `DWORD`, through their little-endian bytes. +//! A handle is an opaque value that no code dereferences, so it converts through its address alone. +use std::ffi::c_void; use std::mem::size_of; +use std::ptr; const _: () = assert!( size_of::() >= size_of::(), @@ -35,6 +38,16 @@ pub(crate) const fn dword_const(value: usize) -> u32 { u32::from_le_bytes([first, second, third, fourth]) } +/// Returns a handle's numeric value, as a child sees it in an argument or the environment. +pub(crate) fn handle_value(handle: *mut c_void) -> isize { + isize::from_ne_bytes(handle.addr().to_ne_bytes()) +} + +/// Returns the handle a numeric value names, a pointer without provenance. +pub(crate) const fn handle_from_value(value: isize) -> *mut c_void { + ptr::without_provenance_mut(usize::from_ne_bytes(value.to_ne_bytes())) +} + #[cfg(kani)] mod proofs { use super::*; @@ -62,6 +75,14 @@ mod proofs { kani::cover!(value == u32::MAX as usize); } + #[kani::proof] + fn every_handle_value_round_trips() { + let value: isize = kani::any(); + assert_eq!(handle_value(handle_from_value(value)), value); + kani::cover!(value == -1); + kani::cover!(value == isize::MIN); + } + #[kani::proof] fn negative_control_bytes_in_big_endian_order() { let value: u32 = kani::any(); @@ -88,6 +109,10 @@ mod tests { assert_eq!(dword_const(0), 0); assert_eq!(dword_const(0x0102_0304), 0x0102_0304); assert_eq!(dword_const(widen(u32::MAX)), u32::MAX); + for value in [0, 4, 0x1234, -1, -2, isize::MIN, isize::MAX] { + assert_eq!(handle_value(handle_from_value(value)), value); + } + assert!(handle_from_value(0).is_null()); } #[test] diff --git a/src/domain/creation_flag_rules.rs b/src/domain/creation_flag_rules.rs index 35e3f39..da408af 100644 --- a/src/domain/creation_flag_rules.rs +++ b/src/domain/creation_flag_rules.rs @@ -151,7 +151,7 @@ const fn distinct_bits(flags: &[u32]) -> bool { /// Fails the build unless the policy, console, priority, and internal flags are distinct bits that no two groups share. /// -/// `creation_word` names it, so every compiler evaluates it; Rust 1.75 counts no use inside `const _`. +/// `creation_word` names it, so every compiler evaluates it; Rust 1.84 counts no use inside `const _`. const LAYOUT: () = assert!( distinct_bits(&POLICY) && distinct_bits(&CONSOLE) diff --git a/src/lib.rs b/src/lib.rs index 01556a2..c6470a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,10 @@ mod plan; #[cfg(windows)] mod resource; #[cfg(windows)] -#[allow(unsafe_code)] +#[expect( + unsafe_code, + reason = "sys is the crate's only Win32 FFI boundary, and it holds BorrowedPseudoConsole::borrow_raw" +)] mod sys; #[cfg(windows)] mod transaction; diff --git a/src/mitigation.rs b/src/mitigation.rs index b54d48b..8ef571d 100644 --- a/src/mitigation.rs +++ b/src/mitigation.rs @@ -451,7 +451,7 @@ const fn disjoint(fields: &[Field]) -> bool { /// Fails the build unless every field fits its word and no two fields of one word share a bit. /// -/// `MitigationPolicy::with` names it, so every compiler evaluates it; Rust 1.75 counts no use inside `const _`. +/// `MitigationPolicy::with` names it, so every compiler evaluates it; Rust 1.84 counts no use inside `const _`. const LAYOUT: () = assert!( disjoint(&FIELDS), "mitigation fields fit their words and share no bit" diff --git a/src/options.rs b/src/options.rs index 2198f85..daadef2 100644 --- a/src/options.rs +++ b/src/options.rs @@ -129,12 +129,12 @@ impl Creation { } } - pub(crate) fn set_priority(&mut self, class: PriorityClass) { + pub(crate) const fn set_priority(&mut self, class: PriorityClass) { self.priority = class.flag(); } /// Sets one policy flag. - pub(crate) fn set(&mut self, flag: u32) { + pub(crate) const fn set(&mut self, flag: u32) { self.policy |= flag; } @@ -277,8 +277,8 @@ mod tests { let options = SpawnOptions::new().job(&outer).job(&inner); let reused = options.clone(); assert_eq!(reused.jobs.len(), 2); - assert!(std::ptr::eq(reused.jobs[0], &outer)); - assert!(std::ptr::eq(reused.jobs[1], &inner)); + assert!(std::ptr::eq(reused.jobs[0], &raw const outer)); + assert!(std::ptr::eq(reused.jobs[1], &raw const inner)); let empty = SpawnOptions::default(); assert!(empty.jobs.is_empty() && empty.parent.is_none() && empty.pseudoconsole.is_none()); } diff --git a/src/resource.rs b/src/resource.rs index 6e0ce39..5fadda8 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -129,7 +129,7 @@ impl Value { /// The value of a handle in the current process's table. pub(crate) fn of(handle: BorrowedHandle<'_>) -> Self { Self { - raw: crate::sys::handle_value(handle.as_raw_handle()), + raw: crate::domain::conversion_rules::handle_value(handle.as_raw_handle()), table: PhantomData, } } diff --git a/src/sys.rs b/src/sys.rs index fd52ad8..7c4cffc 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -47,7 +47,7 @@ use windows_sys::Win32::System::Threading::{ }; use crate::domain::attribute_rules::{self, SizeFault}; -use crate::domain::conversion_rules::{dword_const, widen}; +use crate::domain::conversion_rules::{dword_const, handle_from_value, handle_value, widen}; use crate::domain::creation_flag_rules::{creation_word, ConsoleFlag, PriorityFlag}; use crate::domain::environment_rules; use crate::handles::BorrowedPseudoConsole; @@ -132,7 +132,7 @@ fn duplicate_between( source_process, source, target_process, - &mut duplicate, + &raw mut duplicate, 0, i32::from(inheritable), options, @@ -207,7 +207,7 @@ pub(crate) fn duplicate_remote<'a, K>( GetCurrentProcess(), raw(source.untyped()), raw(target_process.untyped()), - &mut value, + &raw mut value, 0, i32::from(inheritable), DUPLICATE_SAME_ACCESS, @@ -281,7 +281,15 @@ pub(crate) fn create_pipe(parent_reads: bool) -> io::Result { let mut read = ptr::null_mut(); let mut write = ptr::null_mut(); // SAFETY: both output pointers are valid, and null security attributes make both handles non-inheritable. - if unsafe { CreatePipe(&mut read, &mut write, ptr::null::(), 0) } == 0 { + if unsafe { + CreatePipe( + &raw mut read, + &raw mut write, + ptr::null::(), + 0, + ) + } == 0 + { return Err(io::Error::last_os_error()); } // SAFETY: CreatePipe succeeded, so both handles are valid and distinct; they are adopted together. @@ -487,7 +495,7 @@ pub(crate) fn open_job_process( let mut member = 0; // SAFETY: both handles are valid for the query and `member` is writable. bool_result(unsafe { - IsProcessInJob(process.as_raw_handle(), raw(job.untyped()), &mut member) + IsProcessInJob(process.as_raw_handle(), raw(job.untyped()), &raw mut member) })?; Ok((member != 0).then_some(Owned::adopt(process))) } @@ -519,7 +527,7 @@ impl<'a> AttributeList<'a> { let mut bytes = 0_usize; // SAFETY: the documented size query passes a null list. let probed = - unsafe { InitializeProcThreadAttributeList(ptr::null_mut(), count, 0, &mut bytes) }; + unsafe { InitializeProcThreadAttributeList(ptr::null_mut(), count, 0, &raw mut bytes) }; let error = io::Error::last_os_error(); let probe = attribute_rules::probe( probed != 0, @@ -537,7 +545,7 @@ impl<'a> AttributeList<'a> { let pointer = storage.as_mut_ptr().cast(); let mut actual = size_of_val(&*storage); // SAFETY: the `Box<[usize]>` is word-aligned, stable, at least `bytes` long, and owned by the returned `AttributeList`. - if unsafe { InitializeProcThreadAttributeList(pointer, count, 0, &mut actual) } == 0 { + if unsafe { InitializeProcThreadAttributeList(pointer, count, 0, &raw mut actual) } == 0 { return Err(io::Error::last_os_error()); } Ok(Self { @@ -716,8 +724,8 @@ pub(crate) fn create_process(request: &mut ProcessRequest<'_>) -> io::Result) -> io::Result) -> std::io::Result { let mut count = 0; // SAFETY: `process` is valid and `count` is writable DWORD storage. - if unsafe { GetProcessHandleCount(process.as_raw_handle(), &mut count) } == 0 { + if unsafe { GetProcessHandleCount(process.as_raw_handle(), &raw mut count) } == 0 { Err(std::io::Error::last_os_error()) } else { Ok(count) @@ -978,7 +986,7 @@ pub(crate) mod test_support { GetCurrentProcess(), handle.as_raw_handle(), GetCurrentProcess(), - &mut duplicate, + &raw mut duplicate, 0, 0, DUPLICATE_SAME_ACCESS, @@ -998,7 +1006,7 @@ pub(crate) mod test_support { GetCurrentProcess(), handle.as_raw_handle(), GetCurrentProcess(), - &mut duplicate, + &raw mut duplicate, access, 0, 0, @@ -1013,7 +1021,7 @@ pub(crate) mod test_support { pub(crate) fn is_inheritable(handle: BorrowedHandle<'_>) -> bool { let mut flags = 0; // SAFETY: `handle` is valid and `flags` is writable. - let queried = unsafe { GetHandleInformation(handle.as_raw_handle(), &mut flags) }; + let queried = unsafe { GetHandleInformation(handle.as_raw_handle(), &raw mut flags) }; assert_ne!(queried, 0, "GetHandleInformation failed"); flags & HANDLE_FLAG_INHERIT != 0 } @@ -1023,7 +1031,7 @@ pub(crate) mod test_support { let mut read = ptr::null_mut(); let mut write = ptr::null_mut(); // SAFETY: both output pointers are writable, and null security attributes make both handles non-inheritable. - let created = unsafe { CreatePipe(&mut read, &mut write, ptr::null(), 0) }; + let created = unsafe { CreatePipe(&raw mut read, &raw mut write, ptr::null(), 0) }; assert_ne!(created, 0, "CreatePipe failed"); // SAFETY: CreatePipe succeeded, so both handles are new and distinct. unsafe { @@ -1064,7 +1072,8 @@ pub(crate) mod test_support { self.armed = false; let mut code = 0_u32; // SAFETY: the owned process handle is valid and `code` is writable. - let queried = unsafe { GetExitCodeProcess(self.process.as_raw_handle(), &mut code) }; + let queried = + unsafe { GetExitCodeProcess(self.process.as_raw_handle(), &raw mut code) }; assert_ne!(queried, 0, "GetExitCodeProcess failed"); code } @@ -1102,7 +1111,7 @@ pub(crate) mod test_support { input_reader.as_raw_handle(), output_writer.as_raw_handle(), 0, - &mut value, + &raw mut value, ) }; assert!(created >= 0, "CreatePseudoConsole failed with {created:#x}"); @@ -1157,7 +1166,7 @@ pub(crate) fn exit_status(process: Borrowed<'_, Process>) -> io::Result, buffer: &mut [u8]) -> io:: raw(handle.untyped()), buffer.as_mut_ptr(), length, - &mut read, + &raw mut read, ptr::null_mut(), ) } == 0 @@ -1227,7 +1236,7 @@ pub(crate) fn write_handle(handle: Borrowed<'_, Pipe>, buffer: &[u8]) -> io::Res raw(handle.untyped()), buffer.as_ptr(), length, - &mut written, + &raw mut written, ptr::null_mut(), ) } == 0 @@ -1355,21 +1364,6 @@ fn is_valid_handle(handle: HANDLE) -> bool { !handle.is_null() && handle != INVALID_HANDLE_VALUE } -/// Returns a handle's numeric value, as a child sees it in an argument or the environment. -/// -/// Rust 1.75 has no `addr` for pointers, so reading the value takes `as`. -#[allow(clippy::as_conversions)] -pub(crate) fn handle_value(handle: HANDLE) -> isize { - handle as isize -} - -/// Returns the handle a numeric value names. -/// -/// A handle is an opaque value that no code dereferences, so the pointer carries no provenance, as `ptr::without_provenance_mut` would make it. -pub(crate) const fn handle_from_value(value: isize) -> HANDLE { - ptr::null_mut::().wrapping_byte_offset(value) -} - /// Returns true if `error` carries the Win32 error `code`. fn is_win32_error(error: &io::Error, code: u32) -> bool { error diff --git a/tests/argv_roundtrip.rs b/tests/argv_roundtrip.rs index 7166159..3f58d83 100644 --- a/tests/argv_roundtrip.rs +++ b/tests/argv_roundtrip.rs @@ -105,8 +105,8 @@ mod windows { 0, ptr::null(), ptr::null(), - &startup, - &mut information, + &raw const startup, + &raw mut information, ) }; if created == 0 { @@ -125,7 +125,7 @@ mod windows { } let mut code = 0_u32; // SAFETY: the owned process handle is valid and `code` is writable. - if unsafe { GetExitCodeProcess(process.as_raw_handle(), &mut code) } == 0 { + if unsafe { GetExitCodeProcess(process.as_raw_handle(), &raw mut code) } == 0 { return Err(io::Error::last_os_error()); } Ok(code) diff --git a/tests/command_line_parsers.rs b/tests/command_line_parsers.rs index 0bc121d..429ff68 100644 --- a/tests/command_line_parsers.rs +++ b/tests/command_line_parsers.rs @@ -135,7 +135,7 @@ mod windows { fn shell(line: &[u16]) -> io::Result>> { let mut count = 0; // SAFETY: `line` is NUL-terminated and `count` is writable. - let vector = unsafe { CommandLineToArgvW(line.as_ptr(), &mut count) }; + let vector = unsafe { CommandLineToArgvW(line.as_ptr(), &raw mut count) }; if vector.is_null() { return Err(io::Error::last_os_error()); } @@ -201,7 +201,16 @@ mod windows { let mut environment = ptr::null_mut(); let mut startup = 0; // SAFETY: every out pointer is writable. - if unsafe { get(&mut count, &mut vector, &mut environment, 0, &mut startup) } != 0 { + if unsafe { + get( + &raw mut count, + &raw mut vector, + &raw mut environment, + 0, + &raw mut startup, + ) + } != 0 + { return Err(invalid("__wgetmainargs failed".to_owned())); } // SAFETY: __wgetmainargs built `count` NUL-terminated strings. diff --git a/tests/random/mod.rs b/tests/random/mod.rs index b2223bf..b3280c6 100644 --- a/tests/random/mod.rs +++ b/tests/random/mod.rs @@ -34,7 +34,7 @@ impl Generator { Self(seed) } - pub(crate) fn next(&mut self) -> u64 { + pub(crate) const fn next(&mut self) -> u64 { let [first, second, third] = SHIFTS; let Self(state) = self; *state ^= state.wrapping_shr(first); diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 53b3f94..df250ad 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -41,7 +41,7 @@ pub(crate) fn pipe() -> io::Result<(File, File)> { let mut read = ptr::null_mut(); let mut write = ptr::null_mut(); // SAFETY: both output pointers are writable, and null security attributes make both handles non-inheritable. - if unsafe { CreatePipe(&mut read, &mut write, ptr::null(), 0) } == 0 { + if unsafe { CreatePipe(&raw mut read, &raw mut write, ptr::null(), 0) } == 0 { return Err(io::Error::last_os_error()); } // SAFETY: CreatePipe succeeded, so both handles are new and distinct. @@ -66,7 +66,7 @@ pub(crate) fn local_duplicate( GetCurrentProcess(), source.as_handle().as_raw_handle(), GetCurrentProcess(), - &mut duplicate, + &raw mut duplicate, 0, i32::from(inheritable), DUPLICATE_SAME_ACCESS, @@ -166,7 +166,7 @@ impl ProbeCommand { )) } - pub(crate) fn command_mut(&mut self) -> &mut Command { + pub(crate) const fn command_mut(&mut self) -> &mut Command { &mut self.command } @@ -205,8 +205,11 @@ pub(crate) fn run_probe_if_requested() { exit(code); } -/// Ends the probe with `code`; a probe reports by exit code, and no destructor may run after the gate. -#[allow(clippy::disallowed_methods)] +/// Ends the probe with `code`. +#[expect( + clippy::disallowed_methods, + reason = "a probe reports by exit code, and no destructor may run after the gate" +)] fn exit(code: u32) -> ! { std::process::exit(i32::from_ne_bytes(code.to_ne_bytes())) } @@ -307,7 +310,7 @@ impl ProcessExitGuard { self.armed = false; let mut code = 0_u32; // SAFETY: the owned process handle is valid and `code` is writable. - if unsafe { GetExitCodeProcess(self.process.as_raw_handle(), &mut code) } == 0 { + if unsafe { GetExitCodeProcess(self.process.as_raw_handle(), &raw mut code) } == 0 { return Err(io::Error::last_os_error()); } Ok(code) diff --git a/tests/windows_spawn.rs b/tests/windows_spawn.rs index 2a3641d..213dda9 100644 --- a/tests/windows_spawn.rs +++ b/tests/windows_spawn.rs @@ -58,7 +58,8 @@ fn attached_to_test_pseudoconsole() -> bool { }; let mut information = CONSOLE_SCREEN_BUFFER_INFO::default(); // SAFETY: `output` is an open console handle and `information` is writable. - let queried = unsafe { GetConsoleScreenBufferInfo(output.as_raw_handle(), &mut information) }; + let queried = + unsafe { GetConsoleScreenBufferInfo(output.as_raw_handle(), &raw mut information) }; queried != 0 && information.dwSize.X == PCON_SIZE.X && information.dwSize.Y == PCON_SIZE.Y } @@ -76,7 +77,7 @@ fn file_identity(handle: HANDLE) -> io::Result<(u32, u64)> { // SAFETY: the all-zero value is a valid output buffer initialization. let mut information = unsafe { std::mem::zeroed::() }; // SAFETY: the handle is valid and the output buffer is writable for the call. - if unsafe { GetFileInformationByHandle(handle, &mut information) } == 0 { + if unsafe { GetFileInformationByHandle(handle, &raw mut information) } == 0 { Err(io::Error::last_os_error()) } else { let index = @@ -240,19 +241,40 @@ fn native_standard_handle_probe() { let mut byte = [0_u8; 1]; let mut read = 0_u32; // SAFETY: the buffers and byte counts are valid for synchronous I/O. - let read_succeeded = - unsafe { ReadFile(input, byte.as_mut_ptr(), 1, &mut read, std::ptr::null_mut()) }; + let read_succeeded = unsafe { + ReadFile( + input, + byte.as_mut_ptr(), + 1, + &raw mut read, + std::ptr::null_mut(), + ) + }; assert_ne!(read_succeeded, 0); assert_eq!(read, 0); let mut written = 0_u32; // SAFETY: the one-byte buffer and byte count are valid. - let wrote_stdout = - unsafe { WriteFile(output, byte.as_ptr(), 1, &mut written, std::ptr::null_mut()) }; + let wrote_stdout = unsafe { + WriteFile( + output, + byte.as_ptr(), + 1, + &raw mut written, + std::ptr::null_mut(), + ) + }; assert_ne!(wrote_stdout, 0); assert_eq!(written, 1); // SAFETY: the one-byte buffer and byte count are valid. - let wrote_stderr = - unsafe { WriteFile(error, byte.as_ptr(), 1, &mut written, std::ptr::null_mut()) }; + let wrote_stderr = unsafe { + WriteFile( + error, + byte.as_ptr(), + 1, + &raw mut written, + std::ptr::null_mut(), + ) + }; assert_ne!(wrote_stderr, 0); assert_eq!(written, 1); } @@ -342,7 +364,7 @@ fn requested_mitigation_is_visible_on_the_real_process() -> io::Result<()> { fn handle_count() -> io::Result { let mut count = 0; // SAFETY: the pseudo-handle is valid and `count` is writable DWORD storage. - let success = unsafe { GetProcessHandleCount(GetCurrentProcess(), &mut count) }; + let success = unsafe { GetProcessHandleCount(GetCurrentProcess(), &raw mut count) }; if success == 0 { Err(io::Error::last_os_error()) } else { @@ -588,7 +610,7 @@ impl TestPseudoConsole { let mut read = std::ptr::null_mut(); let mut write = std::ptr::null_mut(); // SAFETY: both output pointers are writable; on success each handle is adopted once. - if unsafe { CreatePipe(&mut read, &mut write, std::ptr::null(), 0) } == 0 { + if unsafe { CreatePipe(&raw mut read, &raw mut write, std::ptr::null(), 0) } == 0 { return Err(io::Error::last_os_error()); } // SAFETY: CreatePipe succeeded and returned two distinct handles. @@ -610,7 +632,7 @@ impl TestPseudoConsole { input_reader.as_raw_handle(), output_writer.as_raw_handle(), 0, - &mut value, + &raw mut value, ) }; if result < 0 { @@ -654,7 +676,7 @@ impl TestPseudoConsole { writer.as_raw_handle(), input.as_ptr(), length, - &mut written, + &raw mut written, std::ptr::null_mut(), ) } == 0 diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 6a791fb..912b9eb 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -2,7 +2,7 @@ name = "xtask" version = "0.0.0" edition = "2021" -rust-version = "1.75" +rust-version = "1.84" publish = false [dependencies] diff --git a/xtask/src/gates.rs b/xtask/src/gates.rs index 19ebd01..85ee199 100644 --- a/xtask/src/gates.rs +++ b/xtask/src/gates.rs @@ -5,36 +5,6 @@ use std::fs; use std::io; use std::path::{Path, PathBuf}; -/// A lint exception the repository accepts, with the number of attributes that claim it. -struct Exception { - file: &'static str, - lint: &'static str, - count: usize, - reason: &'static str, -} - -/// Every `#[allow]` in the repository; an attribute outside this table, or a count that differs, fails the gate. -const EXCEPTIONS: &[Exception] = &[ - Exception { - file: "src/lib.rs", - lint: "unsafe_code", - count: 1, - reason: "sys is the only Win32 FFI boundary; it holds BorrowedPseudoConsole::borrow_raw and the test pseudoconsoles", - }, - Exception { - file: "src/sys.rs", - lint: "clippy::as_conversions", - count: 1, - reason: "handle_value reads a handle's value, and Rust 1.75 has no pointer addr", - }, - Exception { - file: "tests/support/mod.rs", - lint: "clippy::disallowed_methods", - count: 1, - reason: "a probe reports by exit code", - }, -]; - /// Identifiers that measure or wait on time (ADR 0010). const TIME_NAMES: [&str; 6] = [ "SystemTime", @@ -114,16 +84,13 @@ const LIBRARY_BANS: [(&[&str], &str); 4] = [ #[derive(Debug, PartialEq, Eq)] pub(crate) struct Finding { pub(crate) file: String, - pub(crate) line: Option, + pub(crate) line: usize, pub(crate) rule: String, } impl fmt::Display for Finding { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.line { - Some(line) => write!(formatter, "{}:{line}: {}", self.file, self.rule), - None => write!(formatter, "{}: {}", self.file, self.rule), - } + write!(formatter, "{}:{}: {}", self.file, self.line, self.rule) } } @@ -135,7 +102,6 @@ pub(crate) fn check(root: &Path) -> io::Result> { } files.sort(); let mut findings = Vec::new(); - let mut allows = Vec::new(); for path in files { let relative = path .strip_prefix(root) @@ -143,11 +109,8 @@ pub(crate) fn check(root: &Path) -> io::Result> { .to_string_lossy() .replace('\\', "/"); let source = fs::read_to_string(&path)?; - let scan = scan(&relative, &source); - findings.extend(scan.findings); - allows.extend(scan.allows); + findings.extend(scan(&relative, &source)); } - findings.extend(check_exceptions(&allows, EXCEPTIONS)); Ok(findings) } @@ -163,50 +126,6 @@ fn collect_rust_files(directory: &Path, files: &mut Vec) -> io::Result< Ok(()) } -/// An `#[allow]` lint and where it appears. -#[derive(Debug, PartialEq, Eq)] -struct Allow { - file: String, - line: usize, - lint: String, -} - -fn check_exceptions(allows: &[Allow], exceptions: &[Exception]) -> Vec { - let mut findings = Vec::new(); - for allow in allows { - let registered = exceptions - .iter() - .any(|exception| exception.file == allow.file && exception.lint == allow.lint); - if !registered { - findings.push(Finding { - file: allow.file.clone(), - line: Some(allow.line), - rule: format!( - "#[allow({})] is not registered in xtask/src/gates.rs", - allow.lint - ), - }); - } - } - for exception in exceptions { - let count = allows - .iter() - .filter(|allow| allow.file == exception.file && allow.lint == exception.lint) - .count(); - if count != exception.count { - findings.push(Finding { - file: exception.file.to_owned(), - line: None, - rule: format!( - "{} is registered {} time(s) ({}) but allowed {count} time(s)", - exception.lint, exception.count, exception.reason - ), - }); - } - } - findings -} - /// The offset of the token after the next one: `dyn` after `Box <`, the arguments after `name (`, and the bracket after `# !`. const AFTER_NEXT: usize = 2; /// Two adjacent tokens, such as `fn source` read backwards. @@ -238,18 +157,13 @@ pub(crate) enum Token { }, } -struct Scan { - findings: Vec, - allows: Vec, -} - -fn scan(file: &str, source: &str) -> Scan { +fn scan(file: &str, source: &str) -> Vec { let tokens = lex(source); let mut findings = Vec::new(); let mut finding = |line: usize, rule: String| { findings.push(Finding { file: file.to_owned(), - line: Some(line), + line, rule, }); }; @@ -260,7 +174,6 @@ fn scan(file: &str, source: &str) -> Scan { .collect(); let guarded = test_only(&code); let library = file.starts_with("src/") && !TEST_ONLY_FILES.contains(&file); - let mut allows = Vec::new(); for (index, (line, token)) in code.iter().enumerate() { let next = |offset: usize| { code.get(index.saturating_add(offset)) @@ -331,18 +244,19 @@ fn scan(file: &str, source: &str) -> Scan { ); } for lint in allowed_lints(&content) { - allows.push(Allow { - file: file.to_owned(), - line: *line, - lint, - }); + finding( + *line, + format!( + "allow({lint}) never expires; write #[expect({lint}, reason = \"...\")] on the one item it concerns (ADR 0013)" + ), + ); } } } Token::Literal(_) | Token::Punct(_) | Token::Comment { .. } => {} } } - Scan { findings, allows } + findings } /// Returns the ADR 0016 rule that the identifier at `index` breaks. @@ -714,7 +628,7 @@ fn second_argument(code: &[&(usize, Token)], after_open: usize) -> Option Vec { let mut lints = Vec::new(); let mut index = 0_usize; @@ -746,6 +660,7 @@ fn allowed_lints(content: &[Token]) -> Vec { } index = index.saturating_add(1); } + lints.retain(|lint| lint != "reason"); lints } @@ -976,19 +891,7 @@ mod tests { use super::*; fn rules(source: &str) -> Vec { - scan("src/example.rs", source) - .findings - .into_iter() - .map(|finding| finding.rule) - .collect() - } - - fn lints(source: &str) -> Vec { - scan("src/example.rs", source) - .allows - .into_iter() - .map(|allow| allow.lint) - .collect() + rules_in("src/example.rs", source) } fn assert_clean(source: &str) { @@ -1020,32 +923,27 @@ mod tests { } #[test] - fn allow_attributes_are_collected_and_checked_against_the_registry() { - assert_eq!( - lints("#[allow(clippy::expect_used, unsafe_code)]\n#![cfg_attr(test, allow(dead_code))]\n"), - ["clippy::expect_used", "unsafe_code", "dead_code"] - ); - let allows = vec![Allow { - file: "src/a.rs".to_owned(), - line: 3, - lint: "unsafe_code".to_owned(), - }]; - let registered = [Exception { - file: "src/a.rs", - lint: "unsafe_code", - count: 1, - reason: "test", - }]; - assert_eq!(check_exceptions(&allows, ®istered), Vec::new()); - assert_eq!(check_exceptions(&allows, &[]).len(), 1); - let twice = [Exception { - file: "src/a.rs", - lint: "unsafe_code", - count: 2, - reason: "test", - }]; - assert_eq!(check_exceptions(&allows, &twice).len(), 1); - assert_eq!(check_exceptions(&[], ®istered).len(), 1); + fn allow_attributes_are_refused_in_every_form() { + let refused = rules(concat!( + "#[allow(clippy::expect_used, unsafe_code)]\n", + "#![cfg_attr(test, allow(dead_code))]\n", + "#![allow(missing_docs, reason = \"stated\")]\n", + )); + assert_eq!(refused.len(), 4, "{refused:?}"); + for lint in [ + "clippy::expect_used", + "unsafe_code", + "dead_code", + "missing_docs", + ] { + assert!( + refused + .iter() + .any(|rule| rule.starts_with(&format!("allow({lint})"))), + "{lint}: {refused:?}" + ); + } + assert_clean("#[expect(unsafe_code, reason = \"the FFI boundary\")]\nmod sys;\n"); } #[test] @@ -1073,7 +971,6 @@ mod tests { fn rules_in(file: &str, source: &str) -> Vec { scan(file, source) - .findings .into_iter() .map(|finding| finding.rule) .collect() diff --git a/xtask/src/proofs/kani.rs b/xtask/src/proofs/kani.rs index 4a3188b..177ac51 100644 --- a/xtask/src/proofs/kani.rs +++ b/xtask/src/proofs/kani.rs @@ -67,6 +67,10 @@ pub(crate) const KANI_PROOFS: &[Proof] = &[ name: "domain::conversion_rules::proofs::every_size_within_a_dword_narrows_to_its_value", covers: 1, }, + Proof { + name: "domain::conversion_rules::proofs::every_handle_value_round_trips", + covers: 2, + }, Proof { name: "domain::creation_flag_rules::proofs::the_word_holds_the_policy_one_console_one_priority_and_the_internal_flags", covers: 2, diff --git a/xtask/src/tasks.rs b/xtask/src/tasks.rs index 0c1fdd5..dc1f911 100644 --- a/xtask/src/tasks.rs +++ b/xtask/src/tasks.rs @@ -187,7 +187,7 @@ fn run_simple(root: &Path, task: SimpleTask) -> Result<()> { } SimpleTask::Msrv => run_cargo_with_toolchain( root, - "1.75", + "1.84", &["check", "--workspace", "--all-targets", "--locked"], ), SimpleTask::CrossTargets => { @@ -500,7 +500,7 @@ fn validate_cyclonedx(path: &Path, package: &PackageInfo) -> Result<()> { if document .get("dependencies") .and_then(Value::as_array) - .map_or(true, Vec::is_empty) + .is_none_or(Vec::is_empty) { return fail("CycloneDX SBOM has no dependency relationships"); } @@ -511,7 +511,7 @@ fn require_licenses(component: &Value) -> Result<()> { if component .get("licenses") .and_then(Value::as_array) - .map_or(true, Vec::is_empty) + .is_none_or(Vec::is_empty) { let name = component .get("name")