Skip to content

Commit 38c7b04

Browse files
authored
Merge branch 'main' into cbmc-update
2 parents e39b6c2 + a95fe3f commit 38c7b04

10 files changed

Lines changed: 658 additions & 138 deletions

File tree

.github/workflows/kani.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ jobs:
6060

6161
perf:
6262
runs-on: ubuntu-24.04
63+
# The full perf suite runs in ~45 min (matching main): most cases finish in
64+
# seconds; the heaviest, s2n-quic-core, runs 34 harnesses in ~1500s. The
65+
# per-test guardrail in scripts/kani-perf.sh (2400s) sits above that. The
66+
# 180-min step timeout leaves ample headroom over that realistic runtime,
67+
# and the job timeout sits above (step timeout) x (max_attempts) so a retry
68+
# can still land.
69+
timeout-minutes: 380
6370
permissions:
6471
contents: read
6572
steps:
@@ -72,7 +79,14 @@ jobs:
7279
os: ubuntu-24.04
7380

7481
- name: Execute Kani performance tests
75-
run: ./scripts/kani-perf.sh
82+
# Retry on infra-driven failures (spot preemption / runner shutdown).
83+
# The script itself bounds per-test wall time, so functional regressions
84+
# surface as normal test failures and are not retried indefinitely.
85+
uses: nick-fields/retry@v3
86+
with:
87+
timeout_minutes: 180
88+
max_attempts: 2
89+
command: ./scripts/kani-perf.sh
7690
env:
7791
RUST_TEST_THREADS: 1
7892

kani-compiler/src/kani_middle/reachability.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ use rustc_public::mir::{
3232
use rustc_public::rustc_internal;
3333
use rustc_public::ty::{Allocation, ClosureKind, ConstantKind, RigidTy, Ty, TyKind};
3434
use rustc_public::{CrateDef, ItemKind};
35+
#[cfg(debug_assertions)]
3536
use rustc_session::config::OutputType;
37+
use std::collections::{HashMap, HashSet};
3638
use std::fmt::{Display, Formatter};
37-
use std::{
38-
collections::{HashMap, HashSet},
39-
fs::File,
40-
io::{BufWriter, Write},
41-
};
39+
#[cfg(debug_assertions)]
40+
use std::fs::File;
41+
#[cfg(debug_assertions)]
42+
use std::io::{BufWriter, Write};
4243

4344
use crate::kani_middle::coercion;
4445
use crate::kani_middle::coercion::CoercionBase;
@@ -580,6 +581,7 @@ impl CallGraph {
580581

581582
/// Print the graph in DOT format to a file.
582583
/// See <https://graphviz.org/doc/info/lang.html> for more information.
584+
#[cfg(debug_assertions)]
583585
fn dump_dot(&self, tcx: TyCtxt, initial: Option<MonoItem>) -> std::io::Result<()> {
584586
if let Ok(target) = std::env::var("KANI_REACH_DEBUG") {
585587
debug!(?target, "dump_dot");
@@ -608,6 +610,7 @@ impl CallGraph {
608610
}
609611

610612
/// Write all notes to the given writer.
613+
#[cfg(debug_assertions)]
611614
fn dump_all<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
612615
tracing::info!(nodes=?self.nodes.len(), edges=?self.edges.len(), "dump_all");
613616
for node in &self.nodes {
@@ -621,6 +624,7 @@ impl CallGraph {
621624
}
622625

623626
/// Write all notes that may have led to the discovery of the given target.
627+
#[cfg(debug_assertions)]
624628
fn dump_reason<W: Write>(&self, writer: &mut W, target: &str) -> std::io::Result<()> {
625629
let mut queue: Vec<Node> =
626630
self.nodes.iter().filter(|item| item.to_string().contains(target)).cloned().collect();

rust-toolchain.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
# SPDX-License-Identifier: Apache-2.0 OR MIT
33

44
[toolchain]
5-
channel = "nightly-2025-12-03"
5+
channel = "nightly-2025-12-04"
66
components = ["llvm-tools", "rustc-dev", "rust-src", "rustfmt"]

scripts/kani-fmt.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ cargo fmt ${check_flag} || error=1
3030
# Check test source files.
3131
TESTS=("tests" "docs/src/tutorial")
3232
# Add ignore patterns for code we don't want to format.
33-
IGNORE=("*/perf/s2n-quic/*")
33+
# `*/perf/s2n-quic/*` excludes the upstream submodule.
34+
# `*/perf/overlays/*` excludes the overlay sources (see
35+
# `tests/perf/overlays/README.md`): these are partial copies of submodule
36+
# files staged for `cp -r` by `scripts/kani-perf.sh`, and they can reference
37+
# `mod` declarations whose siblings only exist in the submodule, so rustfmt
38+
# cannot standalone-parse them.
39+
IGNORE=("*/perf/s2n-quic/*" "*/perf/overlays/*")
3440

3541
# Arguments for the find command for excluding the IGNORE paths
3642
IGNORE_ARGS=()

scripts/kani-perf.sh

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,21 @@ done
2626

2727
suite="perf"
2828
mode="cargo-kani-test"
29-
echo "Check compiletest suite=$suite mode=$mode"
30-
cargo run -p compiletest -- --suite $suite --mode $mode --no-fail-fast
29+
# Bound each test's wall time so a runaway case (e.g. an OOM-prone harness)
30+
# fails as a normal test failure with output, instead of triggering an
31+
# unattributable runner OOM-kill / shutdown signal in CI.
32+
# The cap is a guardrail against runaway/OOM, not a tight performance bound,
33+
# so it must sit above the slowest legitimate test. The whole-crate s2n-quic
34+
# tests run many heavy harnesses back-to-back; s2n-quic-core (34 harnesses,
35+
# e.g. sync::spsc::alloc_test ~375s and inet::checksum::differential ~135s)
36+
# takes ~1500s on CI. It passes cap-free on main, whose full suite is ~2650s,
37+
# and this branch's suite matches that, so this is not a regression. Default
38+
# 2400s (40 min) clears s2n-quic-core with margin while the realistic suite
39+
# runtime (~45 min) stays well within the workflow step timeout.
40+
# Override via KANI_PERF_TEST_TIMEOUT.
41+
timeout_secs="${KANI_PERF_TEST_TIMEOUT:-2400}"
42+
echo "Check compiletest suite=$suite mode=$mode timeout=${timeout_secs}s"
43+
cargo run -p compiletest -- --suite $suite --mode $mode --no-fail-fast --timeout "$timeout_secs"
3144
exit_code=$?
3245

3346
echo "Cleaning up..."
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
//! Minimal reproducer / regression test for a CBMC symbolic-execution
5+
//! constant-propagation limitation exposed by rust-lang/rust#146436
6+
//! ("Slice iter cleanup", first shipped in nightly-2025-12-04).
7+
//!
8+
//! After #146436, `ChunksExact::next` decides `Some`/`None` via
9+
//! `self.v.split_at_checked(chunk_size).and_then(..)` -- i.e. through the
10+
//! niche-encoded discriminant of `Option<(&[T], &[T])>`, which is the payload
11+
//! reference's null-ness. When the slice's data pointer has been turned into a
12+
//! symbolic null-select `(cond ? &obj : NULL)` by an upstream *symbolic-index*
13+
//! `split_at` (whose `None` case is niche-encoded as a null pointer), CBMC's
14+
//! symex can no longer fold that discriminant to a constant. The
15+
//! `chunks_exact` loop then unwinds to the `--unwind` bound instead of its true
16+
//! (concrete) trip count, blowing up symex/SAT.
17+
//!
18+
//! All three harnesses verify successfully -- the property is unchanged. The
19+
//! point of the reproducer is the encoding *cost*: `poisoned_chunks_exact`
20+
//! produces a much larger SAT instance than `control_clean` for the same-sized
21+
//! concrete chunking work. If the underlying CBMC issue is fixed (or the
22+
//! toolchain reverts the niche interaction), `poisoned_chunks_exact` bounds at
23+
//! its true trip count again and the cost collapses back to the control's.
24+
25+
// Clean pointer (no symbolic-index split): the chunking loop bounds at its
26+
// true trip count.
27+
#[kani::proof]
28+
#[kani::unwind(9)]
29+
fn control_clean() {
30+
let chunks: [u8; 4] = kani::any();
31+
let mut sum = 0u64;
32+
for c in chunks.chunks_exact(2) {
33+
sum = sum.wrapping_add(c[0] as u64);
34+
}
35+
kani::cover!(sum != 999);
36+
}
37+
38+
// A symbolic-index `split_at` poisons the slice's data pointer into a
39+
// `(cond ? base : NULL)` select; `chunks_exact` (which decides Some/None via
40+
// `split_at_checked(..).and_then(..)`) then over-unwinds even though `chunks`
41+
// has a concrete length of 4.
42+
#[kani::proof]
43+
#[kani::unwind(9)]
44+
fn poisoned_chunks_exact() {
45+
let arr: [u8; 16] = kani::any();
46+
let idx: usize = kani::any();
47+
kani::assume(idx <= 16);
48+
let (a, _) = arr.split_at(idx);
49+
if a.len() >= 4 {
50+
let chunks = &a[..4]; // concrete length 4, but a poisoned data pointer
51+
let mut sum = 0u64;
52+
for c in chunks.chunks_exact(2) {
53+
sum = sum.wrapping_add(c[0] as u64);
54+
}
55+
kani::cover!(sum != 999);
56+
}
57+
}
58+
59+
// Negative control: the same poisoned slice fed to a direct `split_at_checked`
60+
// loop (no `and_then` closure updating a captured `&mut`) is NOT affected -- it
61+
// bounds normally. This shows both ingredients (the poisoned pointer *and*
62+
// `chunks_exact`'s `and_then` form) are required to trigger the blow-up.
63+
#[kani::proof]
64+
#[kani::unwind(9)]
65+
fn poisoned_split_at_checked() {
66+
let arr: [u8; 16] = kani::any();
67+
let idx: usize = kani::any();
68+
kani::assume(idx <= 16);
69+
let (a, _) = arr.split_at(idx);
70+
if a.len() >= 4 {
71+
let mut v = &a[..4];
72+
let mut c = 0u64;
73+
while let Some((_h, t)) = v.split_at_checked(2) {
74+
v = t;
75+
c += 1;
76+
}
77+
kani::cover!(c != 999);
78+
}
79+
}

0 commit comments

Comments
 (0)