From b339f6feb70ed390fba0af9dd8655ced085dbc17 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:39:55 +0000 Subject: [PATCH 1/5] Raise MSRV to 1.85 Updates the workspace rust-version, CI MSRV builds, installation docs (RHEL 9.7 is now required for a sufficiently new packaged Rust), and the changelog. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01X4EZ7B8sU7cLxPP4qnHfAm --- .github/workflows/ci.yml | 7 +++---- CHANGELOG.rst | 1 + Cargo.toml | 2 +- docs/installation.rst | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 559d49d64a5d..d4c4fa66656a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,11 +50,10 @@ jobs: - {VERSION: "3.14", NOXSESSION: "tests", OPENSSL: {TYPE: "openssl", VERSION: "f31510e95333b33a8765cbb81a147df7572c88b2"}} # Builds with various Rust versions. Includes MSRV and next # potential future MSRV. - # - 1.85: 2024 edition # - 1.95: cfg_select - - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.83.0"} - - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.83.0", OPENSSL: {TYPE: "aws-lc", VERSION: "v5.1.0"}} - - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.83.0", OPENSSL: {TYPE: "boringssl", VERSION: "1c7d52ef3e3f373302cb957089fa783d1e5fd8cd"}} + - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.85.0"} + - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.85.0", OPENSSL: {TYPE: "aws-lc", VERSION: "v5.1.0"}} + - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.85.0", OPENSSL: {TYPE: "boringssl", VERSION: "1c7d52ef3e3f373302cb957089fa783d1e5fd8cd"}} - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "beta"} - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "nightly"} - {VERSION: "3.14", NOXSESSION: "tests-rust-debug"} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fa3f1e0c538d..361fba2bc113 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,6 +39,7 @@ Changelog classes and the classes in :mod:`~cryptography.hazmat.primitives.asymmetric.padding` can now be compared with ``==``. +* Updated the minimum supported Rust version (MSRV) to 1.85.0, from 1.83.0. .. _v49-0-0: diff --git a/Cargo.toml b/Cargo.toml index 540a19e4f089..b41a08dcfe85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ authors = ["The cryptography developers "] edition = "2021" publish = false # This specifies the MSRV -rust-version = "1.83.0" +rust-version = "1.85.0" license = "Apache-2.0 OR BSD-3-Clause" [workspace.dependencies] diff --git a/docs/installation.rst b/docs/installation.rst index 34b86cdd6c1c..dd627bd8dc26 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -137,9 +137,9 @@ available`. .. warning:: - For RHEL and CentOS you must be on version 9.6 or newer for the command + For RHEL and CentOS you must be on version 9.7 or newer for the command below to install a sufficiently new Rust. If your Rust is less than - 1.83.0 please see the :ref:`Rust installation instructions + 1.85.0 please see the :ref:`Rust installation instructions ` for information about installing a newer Rust. .. code-block:: console @@ -321,7 +321,7 @@ Rust a Rust toolchain. Building ``cryptography`` requires having a working Rust toolchain. The current -minimum supported Rust version is 1.83.0. **This is newer than the Rust some +minimum supported Rust version is 1.85.0. **This is newer than the Rust some package managers ship**, so users may need to install with the instructions below. From c0c2d57d6c2158804af5238e7e131c8ca4d2a92e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:41:19 +0000 Subject: [PATCH 2/5] Update Alpine version note for MSRV 1.85 Alpine 3.21 ships Rust 1.83; 3.22 is the first release with a sufficiently new Rust. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01X4EZ7B8sU7cLxPP4qnHfAm --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index dd627bd8dc26..3b5b51e8433b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -110,7 +110,7 @@ available`. .. warning:: - The Rust available by default in Alpine < 3.21 is older than the + The Rust available by default in Alpine < 3.22 is older than the minimum supported version. See the :ref:`Rust installation instructions ` for information about installing a newer Rust. From a9453ff4b2f11e68acba8455dca40a566e1c705e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 21:00:25 +0000 Subject: [PATCH 3/5] Exclude attribute lines from merged Rust coverage rustc 1.84 changed how proc-macro expansions are attributed in coverage maps: the attribute line (e.g. #[derive(...)], #[pyo3::pyclass]) still gets a zero-count line record, but execution of the generated code is no longer attributed to it. The combined coverage gate previously stayed at 100% only because the Rust 1.83 MSRV jobs attributed hits to those lines; with the MSRV at 1.85 no toolchain in the matrix can, so treat attribute lines as non-executable when merging. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01X4EZ7B8sU7cLxPP4qnHfAm --- .github/bin/merge_rust_coverage.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/bin/merge_rust_coverage.py b/.github/bin/merge_rust_coverage.py index e6521026a94b..f2c80ade53b9 100644 --- a/.github/bin/merge_rust_coverage.py +++ b/.github/bin/merge_rust_coverage.py @@ -39,8 +39,11 @@ def arcs(self) -> set[tuple[int, int]]: def get_excluded_lines(filename: str) -> list[int]: """ - Parse source file for NO-COVERAGE-START/END pairs and return excluded - lines. + Parse source file for lines that are excluded from coverage: lines + within NO-COVERAGE-START/END pairs and attribute lines (e.g. + ``#[derive(...)]``, ``#[pyo3::pyclass]``). rustc >= 1.84 emits + zero-count coverage records for the attribute line of proc-macro + expansions, but never attributes execution to it. """ excluded = [] with open(filename) as f: @@ -51,7 +54,7 @@ def get_excluded_lines(filename: str) -> list[int]: in_excluded_block = True elif stripped == "// NO-COVERAGE-END": in_excluded_block = False - elif in_excluded_block: + elif in_excluded_block or stripped.startswith("#["): excluded.append(line_num) return excluded From 99b80a2951175ed44a67987deb5abea7b81fc702 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 21:20:28 +0000 Subject: [PATCH 4/5] Revert "Exclude attribute lines from merged Rust coverage" This reverts commit a9453ff4b2f11e68acba8455dca40a566e1c705e. --- .github/bin/merge_rust_coverage.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/bin/merge_rust_coverage.py b/.github/bin/merge_rust_coverage.py index f2c80ade53b9..e6521026a94b 100644 --- a/.github/bin/merge_rust_coverage.py +++ b/.github/bin/merge_rust_coverage.py @@ -39,11 +39,8 @@ def arcs(self) -> set[tuple[int, int]]: def get_excluded_lines(filename: str) -> list[int]: """ - Parse source file for lines that are excluded from coverage: lines - within NO-COVERAGE-START/END pairs and attribute lines (e.g. - ``#[derive(...)]``, ``#[pyo3::pyclass]``). rustc >= 1.84 emits - zero-count coverage records for the attribute line of proc-macro - expansions, but never attributes execution to it. + Parse source file for NO-COVERAGE-START/END pairs and return excluded + lines. """ excluded = [] with open(filename) as f: @@ -54,7 +51,7 @@ def get_excluded_lines(filename: str) -> list[int]: in_excluded_block = True elif stripped == "// NO-COVERAGE-END": in_excluded_block = False - elif in_excluded_block or stripped.startswith("#["): + elif in_excluded_block: excluded.append(line_num) return excluded From 4aada54b17737c6695bc3db73e3e17df222b3bec Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 21:30:19 +0000 Subject: [PATCH 5/5] Repair Rust coverage for proc-macro expanded functions Starting with Rust 1.84, a function consisting entirely of proc-macro expanded code (the impls generated by #[derive(...)], #[pyo3::pyclass], #[pyo3::pymethods], ...) gets a coverage mapping containing only a residual region on the attribute line, wired to a non-entry counter that never fires. Two consequences corrupt line coverage: - llvm-cov reports such a function as unexecuted even when the raw profile proves it ran, so the attribute line reads as a miss. - Generated impls that are never codegen'd at all (e.g. an unused IntoPyObject impl) leave "unused function" placeholder records whose lone residual region marks the attribute line executable, and nothing can ever cover it. Under Rust 1.83 these lines were covered because the mappings wired the attribute-line regions to the entry counter; the 1.83 MSRV jobs were the last toolchain in the matrix doing so, which is why the combined coverage gate broke when they moved to 1.85. Repair the lcov at collection time using the raw profile as ground truth: trust the profile's function count when the mapping computes zero for a function that ran, and drop attribute lines whose only functions were never codegen'd. Handwritten dead code is unaffected: it registers profile records (with count 0) and keeps its zero-count lines. This replaces the reverted approach of excluding all attribute lines in the merge step, which discarded genuine coverage information. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01X4EZ7B8sU7cLxPP4qnHfAm --- noxfile.py | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/noxfile.py b/noxfile.py index efeed320a3fb..0a63cf39f57c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -347,6 +347,111 @@ def local(session: nox.Session): BIN_EXT = ".exe" if sys.platform == "win32" else "" +def _parse_profdata_function_counts(prof_dump: str) -> dict[str, int]: + # Parses `llvm-profdata show --all-functions` output, which contains + # a block like this for every instrumented function: + # : + # Hash: 0x0123456789abcdef + # Counters: 7 + # Function count: 8 + counts: dict[str, int] = {} + name = None + for line in prof_dump.splitlines(): + if line.startswith(" ") and not line.startswith(" "): + name = line.strip().removesuffix(":") + elif name is not None and line.strip().startswith("Function count: "): + count = int(line.rsplit(":", 1)[1]) + counts[name] = max(counts.get(name, 0), count) + name = None + return counts + + +def _repair_expanded_code_coverage( + lcov_data: str, prof_counts: dict[str, int] +) -> str: + """ + Starting with Rust 1.84, a function consisting entirely of + proc-macro expanded code (e.g. the impls generated by + ``#[derive(...)]`` or ``#[pyo3::pymethods]``) gets a coverage + mapping containing only a residual region on the attribute line, + wired to a counter that never executes. llvm-cov therefore reports + the function -- and with it the attribute line -- as unexecuted, + even when the raw profile proves the function ran (its counters + are live; only the mapping is defective). We repair the two ways + this corrupts line coverage: + + - If the raw profile's count for a function disagrees with the + lcov function summary (FNDA), trust the profile and mark the + function's start line as executed. + - If every function on a zero-count line is absent from the raw + profile, the compiler emitted only "unused function" placeholder + records for macro-generated impls that were never codegen'd + (e.g. an unused generated trait impl); the line's executability + is an artifact of the residual region, so drop it. Handwritten + dead code is unaffected: it is codegen'd, so it registers a + profile record (with count 0) and its body lines keep their + zero-count records either way. + """ + result: list[str] = [] + record: list[str] = [] + for line in lcov_data.splitlines(): + record.append(line) + if line == "end_of_record": + result.extend(_repair_lcov_record(record, prof_counts)) + record = [] + assert not record + return "".join(f"{line}\n" for line in result) + + +def _repair_lcov_record( + record: list[str], prof_counts: dict[str, int] +) -> list[str]: + fn_start_lines: dict[str, int] = {} + mapped_counts: dict[str, int] = {} + for line in record: + if line.startswith("FN:"): + start, _, name = line.removeprefix("FN:").partition(",") + fn_start_lines[name] = int(start) + elif line.startswith("FNDA:"): + count, _, name = line.removeprefix("FNDA:").partition(",") + mapped_counts[name] = max(mapped_counts.get(name, 0), int(count)) + + repaired_lines: dict[int, int] = {} + line_has_profiled_fn: dict[int, bool] = {} + for fn_name, fn_start in fn_start_lines.items(): + if fn_name in prof_counts: + line_has_profiled_fn[fn_start] = True + else: + line_has_profiled_fn.setdefault(fn_start, False) + if mapped_counts.get(fn_name) == 0: + true_count = prof_counts.get(fn_name, 0) + if true_count > 0: + repaired_lines[fn_start] = max( + repaired_lines.get(fn_start, 0), true_count + ) + unmapped_lines = { + start + for start, profiled in line_has_profiled_fn.items() + if not profiled + } + if not repaired_lines and not unmapped_lines: + return record + + result = [] + for line in record: + if line.startswith("DA:"): + line_number, _, count = line.removeprefix("DA:").partition(",") + if int(count) == 0: + if int(line_number) in repaired_lines: + line = ( + f"DA:{line_number},{repaired_lines[int(line_number)]}" + ) + elif int(line_number) in unmapped_lines: + continue + result.append(line) + return result + + def process_rust_coverage( session: nox.Session, rust_binaries: list[str], @@ -393,5 +498,17 @@ def process_rust_coverage( lambda m: "SF:src/rust/" + m.group(1).replace("\\", "/"), lcov_data.replace("\r\n", "\n"), ) + prof_dump = session.run( + str(target_bindir / ("llvm-profdata" + BIN_EXT)), + "show", + "--all-functions", + "rust-cov.profdata", + silent=True, + external=True, + ) + assert isinstance(prof_dump, str) + lcov_data = _repair_expanded_code_coverage( + lcov_data, _parse_profdata_function_counts(prof_dump) + ) with open(f"{uuid.uuid4()}.lcov", "w") as f: f.write(lcov_data)